From 1457373315c6a6e320fe12c27e5ed69a9fe229ad Mon Sep 17 00:00:00 2001 From: Luc Didry Date: Tue, 30 Apr 2024 08:36:55 +0200 Subject: [PATCH 01/22] =?UTF-8?q?=F0=9F=92=84=F0=9F=93=AF=20=E2=80=94=20Im?= =?UTF-8?q?prove=20notifications=20and=20result(s)=20pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 ++ argos/server/alerting.py | 8 ++++++-- argos/server/templates/result.html | 2 +- argos/server/templates/results.html | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 608f214..3321d21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +- 💄📯 — Improve notifications and result(s) pages + ## 0.1.1 Date: 2024-04-30 diff --git a/argos/server/alerting.py b/argos/server/alerting.py index 96f7965..173b4b1 100644 --- a/argos/server/alerting.py +++ b/argos/server/alerting.py @@ -52,7 +52,9 @@ Status: {severity} Time: {result.submitted_at} Previous status: {old_severity} -See results of task on {request.url_for('get_task_results_view', task_id=task.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} """ mail = f"""\ @@ -107,7 +109,9 @@ Status: {severity} Time: {result.submitted_at} Previous status: {old_severity} -See results of task on {request.url_for('get_task_results_view', task_id=task.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} """ payload = {"title": subject, "message": msg, "priority": priority} diff --git a/argos/server/templates/result.html b/argos/server/templates/result.html index 049e206..abd7d53 100644 --- a/argos/server/templates/result.html +++ b/argos/server/templates/result.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% block title %}

{{ result }}

{% endblock title %} +{% block title %}

Result {{ result.id }} - {{ result.status }}

{% endblock title %} {% block content %}
Task
diff --git a/argos/server/templates/results.html b/argos/server/templates/results.html index 36ce6e9..821f365 100644 --- a/argos/server/templates/results.html +++ b/argos/server/templates/results.html @@ -13,7 +13,7 @@ {% for result in results %} - + {{ result.submitted_at }} {{ result.status }} {{ result.severity }} From 76614a2a509e1d607ea4008b83d91727024dd6ac Mon Sep 17 00:00:00 2001 From: Luc Didry Date: Thu, 23 May 2024 15:41:13 +0200 Subject: [PATCH 02/22] =?UTF-8?q?=F0=9F=93=9D=20=E2=80=94=20Add=20screensh?= =?UTF-8?q?ot=20on=20README=20(fix=20#39)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e88108a..897cbc1 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ A monitoring and status board for your websites. +![Screenshot of Argos’ status page](docs/dashboard.jpg) + 1. Define a list of websites to monitor 2. Specify a list of checks to run on these websites. 3. Argos will run the checks periodically and alert you if something goes wrong. From 415c37bdb4ee2da0fe2ed9b3f0b59c0a65b321a4 Mon Sep 17 00:00:00 2001 From: Luc Didry Date: Thu, 23 May 2024 16:35:41 +0200 Subject: [PATCH 03/22] =?UTF-8?q?=F0=9F=92=84=E2=80=94=20Prevent=20the=20r?= =?UTF-8?q?efresh=20delay=20to=20be=20under=205=20seconds=20(fix=20#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- argos/server/routes/views.py | 4 +++- argos/server/templates/index.html | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/argos/server/routes/views.py b/argos/server/routes/views.py index 4029c0e..f2ef269 100644 --- a/argos/server/routes/views.py +++ b/argos/server/routes/views.py @@ -168,5 +168,7 @@ async def set_refresh_cookies_view( status_code=status.HTTP_303_SEE_OTHER, ) response.set_cookie(key="auto_refresh_enabled", value=auto_refresh_enabled) - response.set_cookie(key="auto_refresh_seconds", value=int(auto_refresh_seconds)) + response.set_cookie( + key="auto_refresh_seconds", value=max(5, int(auto_refresh_seconds)) + ) return response diff --git a/argos/server/templates/index.html b/argos/server/templates/index.html index 35b2fe0..1794899 100644 --- a/argos/server/templates/index.html +++ b/argos/server/templates/index.html @@ -26,7 +26,7 @@ name="auto_refresh_seconds" type="number" form="refresh-form" - min="1" + min="5" value="{{ auto_refresh_seconds }}"> seconds From 4d806e11aa2fe636f1d96116c3638d1b440d3e33 Mon Sep 17 00:00:00 2001 From: Luc Didry Date: Mon, 27 May 2024 08:26:44 +0200 Subject: [PATCH 04/22] =?UTF-8?q?=F0=9F=93=9D=20=E2=80=94=20Make=20.env=20?= =?UTF-8?q?example=20file=20usable=20(fix=20#40)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- conf/.env.example | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/conf/.env.example b/conf/.env.example index f53aca5..1dde480 100644 --- a/conf/.env.example +++ b/conf/.env.example @@ -1,4 +1,7 @@ -ARGOS_YAML_FILE = "my-config.yaml" -ARGOS_DATABASE_URL = "postgresql://argos:argos@localhost/argos" -DB_POOL_SIZE = 10 # https://docs.sqlalchemy.org/en/20/core/pooling.html#sqlalchemy.pool.QueuePool.params.pool_size -DB_MAX_OVERFLOW = 20 \ No newline at end of file +ARGOS_YAML_FILE="my-config.yaml" +ARGOS_DATABASE_URL="postgresql://argos:argos@localhost/argos" + +# https://docs.sqlalchemy.org/en/20/core/pooling.html#sqlalchemy.pool.QueuePool.params.pool_size +DB_POOL_SIZE=10 +# https://docs.sqlalchemy.org/en/20/core/pooling.html#sqlalchemy.pool.QueuePool.params.max_overflow +DB_MAX_OVERFLOW=20 From 3ce293b5aa1c1a2eb903ad483c53e7dd8a21ba9e Mon Sep 17 00:00:00 2001 From: Luc Didry Date: Mon, 27 May 2024 09:42:31 +0200 Subject: [PATCH 05/22] =?UTF-8?q?=F0=9F=93=9D=20=E2=80=94=20Add=20FAQ=20an?= =?UTF-8?q?d=20License=20page=20in=20the=20docs=20(fix=20#44)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/developer/license.md | 5 +++++ docs/faq.md | 34 ++++++++++++++++++++++++++++++++++ docs/index.md | 2 ++ 3 files changed, 41 insertions(+) create mode 100644 docs/developer/license.md create mode 100644 docs/faq.md diff --git a/docs/developer/license.md b/docs/developer/license.md new file mode 100644 index 0000000..4acfe2b --- /dev/null +++ b/docs/developer/license.md @@ -0,0 +1,5 @@ +# License + +Argos is licensed under the terms of the GNU AFFERO GPLv3. + +See [LICENSE file](https://framagit.org/framasoft/framaspace/argos/-/blob/main/LICENSE) on the repository. diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000..da6c556 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,34 @@ +# FAQ + +## How is it different than Nagios? + +In a few words, Argos do less things than Nagios, but it makes it more simple. + +Nagios can do a lot more than Argos, as it can monitor the load of a server, its disk occupation and so much more. +You can extend the possibilities of Nagios with your own plugins, allowing to monitor almost everything. +Argos can only monitor web sites, in various ways (check the HTTP status, check the certificate validity time…). + +On the other hand, configuration and deployment of Argos are very much simpler than Nagios’. + +## How is it different than statping-ng or Uptime Kuma? + +In one word: scalability. + +While [statping-ng](https://statping-ng.github.io/) and [Uptime Kumap](https://uptime.kuma.pet/) have a similar goal than Argos, you can’t monitor thousands of web sites with them efficiently as their dashboard wants to present you the results of all of your web sites at once… and with the history of the results. + +We give those solutions a try, but fetching thousand of results from the dashboard made the backend overloads. + +## Who created Argos? + +### Framasoft + +Framasoft is a non-profit association founded in 2004, financed by [donations](https://support.framasoft.org/), which is limited to a dozen employees and about thirty volunteers (a group of friends!). +You can find more informations on . + +We needed a very efficient web sites monitoring tool for one of our project, but didn’t had time to develop it, so we hired [Alexis Métaireau](#alexis-metaireau) for that. + +### Alexis Métaireau + +Alexis is a long-time free software developer, who has worked for Mozilla, created [Pelican](http://getpelican.com/), a static site generator, [I Hate Money](http://ihatemoney.org/), a website for managing group expenses and many more other projects. + +See for more informations about him. diff --git a/docs/index.md b/docs/index.md index 6d09e90..4366162 100644 --- a/docs/index.md +++ b/docs/index.md @@ -37,6 +37,7 @@ installation/postgresql cli api changelog +faq ``` ```{toctree} @@ -65,5 +66,6 @@ developer/models developer/migrations developer/tests developer/release +developer/license ``` From 4eb802a48b0d51aa2b86c63939b79daef912cb95 Mon Sep 17 00:00:00 2001 From: Luc Didry Date: Mon, 27 May 2024 09:54:08 +0200 Subject: [PATCH 06/22] =?UTF-8?q?=F0=9F=93=9D=20=E2=80=94=20Document=20how?= =?UTF-8?q?=20to=20add=20a=20new=20notification=20way=20(fix=20#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/developer/new-notification-way.md | 34 ++++++++++++++++++++++++++ docs/index.md | 1 + 2 files changed, 35 insertions(+) create mode 100644 docs/developer/new-notification-way.md diff --git a/docs/developer/new-notification-way.md b/docs/developer/new-notification-way.md new file mode 100644 index 0000000..3e193a8 --- /dev/null +++ b/docs/developer/new-notification-way.md @@ -0,0 +1,34 @@ +# Add a notification way + +Adding a new notification way is quite simple. + +First, you need to think about how you will configure it. +As example, here’s how gotify notifications are configured: +```yaml +gotify: + - url: https://example.org + tokens: + - foo + - bar +``` + +Feel free to open an issue to discuss about your notification way or its configuration before coding! +See [#50](https://framagit.org/framasoft/framaspace/argos/-/issues/50) for example. + +Then, you’ll need to add the pydantic schema matching your config in [`argos/schemas/config.py`](https://framagit.org/framasoft/framaspace/argos/-/blob/main/argos/schemas/config.py). + +For gotify, it’s: +```python +class GotifyUrl(BaseModel): + url: HttpUrl + tokens: List[str] +``` + +Add the schema to the `General` schema in the same file (don’t forget to make it optional). + +For gotify, we added this: +```python + gotify: Optional[List[GotifyUrl]] = None +``` + +Finally, write a function which use your new notification way in [`argos/server/alerting.py`](https://framagit.org/framasoft/framaspace/argos/-/blob/main/argos/server/alerting.py) and use it in the `handle_alert` function of the same file. diff --git a/docs/index.md b/docs/index.md index 6d09e90..ef25728 100644 --- a/docs/index.md +++ b/docs/index.md @@ -61,6 +61,7 @@ developer/installation developer/overview developer/dependencies developer/new-check +developer/new-notification-way developer/models developer/migrations developer/tests From 05a8a7bd2eb8bc5c6ab36666ecfbcc5af91c6d1e Mon Sep 17 00:00:00 2001 From: Luc Didry Date: Mon, 27 May 2024 15:25:06 +0200 Subject: [PATCH 07/22] =?UTF-8?q?=F0=9F=94=8A=20=E2=80=94=20Add=20level=20?= =?UTF-8?q?of=20log=20before=20the=20log=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + argos/logging.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3321d21..7954f1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] - 💄📯 — Improve notifications and result(s) pages +- 🔊 — Add level of log before the log message ## 0.1.1 diff --git a/argos/logging.py b/argos/logging.py index 74de43c..071b003 100644 --- a/argos/logging.py +++ b/argos/logging.py @@ -2,6 +2,9 @@ import logging LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] +# Print level before message +logging.basicConfig(format="%(levelname)s: %(message)s") + # XXX We probably want different loggers for client and server. logger = logging.getLogger(__name__) From 7bfe676b5c3f6d78395fea6ed45ea13f8e454e5d Mon Sep 17 00:00:00 2001 From: Luc Didry Date: Thu, 23 May 2024 16:12:12 +0200 Subject: [PATCH 08/22] =?UTF-8?q?=F0=9F=94=8A=20=E2=80=94=20Add=20a=20warn?= =?UTF-8?q?ing=20messages=20in=20the=20logs=20if=20there=20is=20no=20tasks?= =?UTF-8?q?=20in=20database.=20(fix=20#41)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + argos/server/main.py | 13 +++++++++++-- argos/server/queries.py | 4 ++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7954f1d..d452ff3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - 💄📯 — Improve notifications and result(s) pages - 🔊 — Add level of log before the log message +— 🔊 — Add a warning messages in the logs if there is no tasks in database. (fix #41) ## 0.1.1 diff --git a/argos/server/main.py b/argos/server/main.py index 856a116..e91b226 100644 --- a/argos/server/main.py +++ b/argos/server/main.py @@ -8,7 +8,7 @@ from sqlalchemy import create_engine, event from sqlalchemy.orm import sessionmaker from argos.logging import logger -from argos.server import models, routes +from argos.server import models, routes, queries from argos.server.settings import get_app_settings, read_yaml_config @@ -49,7 +49,16 @@ def create_start_app_handler(appli): async def _get_db(): setup_database(appli) - return await connect_to_db(appli) + db = await connect_to_db(appli) + + tasks_count = await queries.count_tasks(db, all_tasks=True) + if tasks_count == 0: + logger.warning( + "There is no tasks in the database. " + 'Please launch the command "argos server reload-config"' + ) + + return db return _get_db diff --git a/argos/server/queries.py b/argos/server/queries.py index cfb3f68..fc6266c 100644 --- a/argos/server/queries.py +++ b/argos/server/queries.py @@ -48,11 +48,11 @@ async def create_result(db: Session, agent_result: schemas.AgentResult, agent_id return result -async def count_tasks(db: Session, selected=False): +async def count_tasks(db: Session, selected=False, all_tasks=False): query = db.query(Task) if selected: query = query.filter(Task.selected_by is not None) - else: + elif not all_tasks: query = query.filter(Task.selected_by is None) return query.count() From be492ed2eec974f85764149b6323a1d291ab54f3 Mon Sep 17 00:00:00 2001 From: Luc Didry Date: Mon, 27 May 2024 10:54:08 +0200 Subject: [PATCH 09/22] =?UTF-8?q?=F0=9F=8E=A8=20=E2=80=94=20Modify=20count?= =?UTF-8?q?=5Ftasks=20behavior,=20improve=20its=20signature?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- argos/server/main.py | 2 +- argos/server/queries.py | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/argos/server/main.py b/argos/server/main.py index e91b226..f3d5d14 100644 --- a/argos/server/main.py +++ b/argos/server/main.py @@ -51,7 +51,7 @@ def create_start_app_handler(appli): db = await connect_to_db(appli) - tasks_count = await queries.count_tasks(db, all_tasks=True) + tasks_count = await queries.count_tasks(db) if tasks_count == 0: logger.warning( "There is no tasks in the database. " diff --git a/argos/server/queries.py b/argos/server/queries.py index fc6266c..f9c07f5 100644 --- a/argos/server/queries.py +++ b/argos/server/queries.py @@ -1,7 +1,7 @@ """Functions to ease SQL queries management""" from datetime import datetime, timedelta from hashlib import sha256 -from typing import List +from typing import List, Union from urllib.parse import urljoin from sqlalchemy import desc, func @@ -48,12 +48,13 @@ async def create_result(db: Session, agent_result: schemas.AgentResult, agent_id return result -async def count_tasks(db: Session, selected=False, all_tasks=False): +async def count_tasks(db: Session, selected: Union[None, bool] = None): query = db.query(Task) - if selected: - query = query.filter(Task.selected_by is not None) - elif not all_tasks: - query = query.filter(Task.selected_by is None) + if selected is not None: + if selected: + query = query.filter(Task.selected_by is not None) + else: + query = query.filter(Task.selected_by is None) return query.count() From cb0a638545449eaf1a31866b693e8810db713eab Mon Sep 17 00:00:00 2001 From: Luc Didry Date: Mon, 27 May 2024 14:55:32 +0200 Subject: [PATCH 10/22] =?UTF-8?q?=E2=9C=A8=20=E2=80=94=20Add=20command=20t?= =?UTF-8?q?o=20generate=20example=20configuration=20(fix=20#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + argos/commands.py | 17 +++++++ argos/config-example.yaml | 66 ++++++++++++++++++++++++++++ conf/config-example.yaml | 64 +-------------------------- docs/installation/getting-started.md | 4 +- 5 files changed, 87 insertions(+), 65 deletions(-) create mode 100644 argos/config-example.yaml mode change 100644 => 120000 conf/config-example.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index d452ff3..6db89ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - 💄📯 — Improve notifications and result(s) pages - 🔊 — Add level of log before the log message — 🔊 — Add a warning messages in the logs if there is no tasks in database. (fix #41) +- ✨ — Add command to generate example configuration (fix #38) ## 0.1.1 diff --git a/argos/commands.py b/argos/commands.py index 2b822b7..51e9f95 100644 --- a/argos/commands.py +++ b/argos/commands.py @@ -242,5 +242,22 @@ async def generate_token(): click.echo(uuid4()) +@server.command() +@coroutine +async def generate_config(): + """Output an example config file. + + \b + Redirect the output to a file to save it: + argos server generate-config > /etc/argos/config.yaml + """ + current_dir = os.path.dirname(__file__) + with open( + os.path.join(current_dir, "config-example.yaml"), "r", encoding="utf-8" + ) as f: + print(f.read()) + f.close() + + if __name__ == "__main__": cli() diff --git a/argos/config-example.yaml b/argos/config-example.yaml new file mode 100644 index 0000000..d36ad85 --- /dev/null +++ b/argos/config-example.yaml @@ -0,0 +1,66 @@ +general: + frequency: "1m" # Run checks every minute. + # Which way do you want to be warned when a check goes to that severity? + # "local" emits a message in the server log + alerts: + ok: + - local + warning: + - local + critical: + - local + unknown: + - 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: + secrets: + # Secrets can be generated using `argos server generate-token`. + # You need at least one. Write them as a list, like: + # - secret_token + +ssl: + thresholds: + - "1d": critical + - "5d": warning + +# It's also possible to define the checks in another file +# with the include syntax: +# +# websites: !include websites.yaml +# +websites: + - domain: "https://mypads.example.org" + paths: + - path: "/mypads/" + checks: + - status-is: 200 + - body-contains: '
' + - ssl-certificate-expiration: "on-check" + - path: "/admin/" + checks: + - status-is: 401 + - domain: "https://munin.example.org" + paths: + - path: "/" + checks: + - status-is: 301 + - path: "/munin/" + checks: + - status-is: 401 diff --git a/conf/config-example.yaml b/conf/config-example.yaml deleted file mode 100644 index 8bbf908..0000000 --- a/conf/config-example.yaml +++ /dev/null @@ -1,63 +0,0 @@ -general: - frequency: "1m" # Run checks every minute. - # Which way do you want to be warned when a check goes to that severity? - alerts: - ok: - - local - warning: - - local - critical: - - local - unknown: - - 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: - secrets: - # Secrets can be generated using `openssl rand -base64 32`. - -ssl: - thresholds: - - "1d": critical - - "5d": warning - -# It's also possible to define the checks in another file -# with the include syntax: -# -# websites: !include websites.yaml -# -websites: - - domain: "https://mypads.example.org" - paths: - - path: "/mypads/" - checks: - - status-is: 200 - - body-contains: '
' - - ssl-certificate-expiration: "on-check" - - path: "/admin/" - checks: - - status-is: 401 - - domain: "https://munin.example.org" - paths: - - path: "/" - checks: - - status-is: 301 - - path: "/munin/" - checks: - - status-is: 401 diff --git a/conf/config-example.yaml b/conf/config-example.yaml new file mode 120000 index 0000000..52bab49 --- /dev/null +++ b/conf/config-example.yaml @@ -0,0 +1 @@ +../argos/config-example.yaml \ No newline at end of file diff --git a/docs/installation/getting-started.md b/docs/installation/getting-started.md index 5f784eb..e65eeec 100644 --- a/docs/installation/getting-started.md +++ b/docs/installation/getting-started.md @@ -30,10 +30,10 @@ pip install -e . ## Configure -The quickest way to get started is to get the `config-example.yaml` file from our repository and edit it: +The quickest way to get started is to generate the configuration file from argos and edit it: ```bash -wget https://framagit.org/framasoft/framaspace/argos/-/raw/main/conf/config-example.yaml -O config.yaml +argos server generate-config > config.yaml ``` You can read more about the configuration in the [configuration section](../configuration.md). From f52dd5dd8aa8303324e30784e37b4fa0bdd796d1 Mon Sep 17 00:00:00 2001 From: Luc Didry Date: Tue, 28 May 2024 10:00:22 +0200 Subject: [PATCH 11/22] =?UTF-8?q?=F0=9F=8E=A8=20=E2=80=94=20Use=20pathlib.?= =?UTF-8?q?Path=20instead=20of=20os.path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- argos/commands.py | 17 ++++++++--------- argos/server/main.py | 4 ++-- argos/server/routes/views.py | 6 +++--- argos/server/settings.py | 6 +++--- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/argos/commands.py b/argos/commands.py index 51e9f95..cf9d976 100644 --- a/argos/commands.py +++ b/argos/commands.py @@ -1,6 +1,7 @@ import asyncio import os from functools import wraps +from pathlib import Path from uuid import uuid4 import click @@ -32,10 +33,11 @@ def coroutine(f): def validate_config_access(ctx, param, value): - if os.path.isfile(value) and os.access(value, os.R_OK): + path = Path(value) + if path.is_file() and os.access(path, os.R_OK): return value - if os.path.isfile(value): + if path.is_file(): raise click.BadParameter(f"the file {value} is not readabale.") raise click.BadParameter(f"the file {value} does not exists or is not reachable.") @@ -226,8 +228,8 @@ async def migrate(config): settings = get_app_settings() - current_dir = os.path.dirname(__file__) - alembic_cfg = Config(os.path.join(current_dir, "server/migrations/alembic.ini")) + current_dir = Path(__file__).resolve().parent + alembic_cfg = Config(current_dir / "server" / "migrations" / "alembic.ini") alembic_cfg.set_main_option("sqlalchemy.url", settings.database_url) command.upgrade(alembic_cfg, "head") @@ -251,12 +253,9 @@ async def generate_config(): Redirect the output to a file to save it: argos server generate-config > /etc/argos/config.yaml """ - current_dir = os.path.dirname(__file__) - with open( - os.path.join(current_dir, "config-example.yaml"), "r", encoding="utf-8" - ) as f: + config_example = Path(__file__).resolve().parent / "config-example.yaml" + with config_example.open("r", encoding="utf-8") as f: print(f.read()) - f.close() if __name__ == "__main__": diff --git a/argos/server/main.py b/argos/server/main.py index f3d5d14..e6afac5 100644 --- a/argos/server/main.py +++ b/argos/server/main.py @@ -1,5 +1,5 @@ -import os import sys +from pathlib import Path from fastapi import FastAPI from fastapi.staticfiles import StaticFiles @@ -35,7 +35,7 @@ def get_application() -> FastAPI: appli.include_router(routes.api, prefix="/api") appli.include_router(routes.views) - static_dir = os.path.join(os.path.dirname(__file__), "static") + static_dir = Path(__file__).resolve().parent / "static" appli.mount("/static", StaticFiles(directory=static_dir), name="static") return appli diff --git a/argos/server/routes/views.py b/argos/server/routes/views.py index f2ef269..273a9bd 100644 --- a/argos/server/routes/views.py +++ b/argos/server/routes/views.py @@ -1,7 +1,7 @@ """Web interface for humans""" from collections import defaultdict from functools import cmp_to_key -from os import path +from pathlib import Path from typing import Annotated from urllib.parse import urlparse @@ -18,8 +18,8 @@ from argos.server.routes.dependencies import get_config, get_db route = APIRouter() -current_dir = path.dirname(__file__) -templates = Jinja2Templates(directory=path.join(current_dir, "../templates")) +current_dir = Path(__file__).resolve().parent +templates = Jinja2Templates(directory=current_dir / ".." / "templates") SEVERITY_LEVELS = {"ok": 1, "warning": 2, "critical": 3, "unknown": 4} diff --git a/argos/server/settings.py b/argos/server/settings.py index 501cae9..12a7286 100644 --- a/argos/server/settings.py +++ b/argos/server/settings.py @@ -1,7 +1,7 @@ """Pydantic schemas for server""" -import os from functools import lru_cache from os import environ +from pathlib import Path from typing import Optional, Union import yaml @@ -77,9 +77,9 @@ def read_yaml_config(filename): def _load_yaml(filename): - base_dir = os.path.dirname(filename) + base_dir = Path(filename).resolve().parent YamlIncludeConstructor.add_to_loader_class( - loader_class=yaml.FullLoader, base_dir=base_dir + loader_class=yaml.FullLoader, base_dir=str(base_dir) ) with open(filename, "r", encoding="utf-8") as stream: From 01ffcc2d28192cd1d1c47515f708dd7ec1b6b49b Mon Sep 17 00:00:00 2001 From: Luc Didry Date: Mon, 27 May 2024 08:44:43 +0200 Subject: [PATCH 12/22] =?UTF-8?q?=F0=9F=93=9D=20=E2=80=94=20TL;DR=20docume?= =?UTF-8?q?ntation=20(fix=20#43=20and=20#52)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + conf/default-argos-agent | 5 + conf/default-argos-server | 6 ++ conf/systemd-agent.service | 16 ++- conf/systemd-server.service | 23 ++-- docs/cli.md | 11 +- docs/deployment/systemd.md | 12 +++ docs/faq.md | 2 +- docs/index.md | 1 + docs/installation/getting-started.md | 2 + docs/installation/tl-dr.md | 155 +++++++++++++++++++++++++++ 11 files changed, 206 insertions(+), 28 deletions(-) create mode 100644 conf/default-argos-agent create mode 100644 conf/default-argos-server create mode 100644 docs/installation/tl-dr.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 6db89ab..602fa74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - 🔊 — Add level of log before the log message — 🔊 — Add a warning messages in the logs if there is no tasks in database. (fix #41) - ✨ — Add command to generate example configuration (fix #38) +- 📝 — Improve documentation ## 0.1.1 diff --git a/conf/default-argos-agent b/conf/default-argos-agent new file mode 100644 index 0000000..dcd5af7 --- /dev/null +++ b/conf/default-argos-agent @@ -0,0 +1,5 @@ +ARGOS_AGENT_TOKEN=Secret +ARGOS_AGENT_SERVER_URL=http://127.0.0.1:8000 +ARGOS_AGENT_LOGLEVEL=WARNING +ARGOS_AGENT_MAX_TASKS=20 +ARGOS_AGENT_WAIT_TIME=10 diff --git a/conf/default-argos-server b/conf/default-argos-server new file mode 100644 index 0000000..6411fc6 --- /dev/null +++ b/conf/default-argos-server @@ -0,0 +1,6 @@ +ARGOS_YAML_FILE="/etc/argos/config.yaml" +ARGOS_DATABASE_URL="postgresql://argos:THE_DB_PASSWORD@localhost/argos" +ARGOS_SERVER_WORKERS=4 +ARGOS_SERVER_SOCKET=127.0.0.1:8000 +# Comma separated list of IP addresses of the web proxy (usually Nginx) +ARGOS_SERVER_FORWARDED_ALLOW_IPS=127.0.0.1 diff --git a/conf/systemd-agent.service b/conf/systemd-agent.service index 93e7e9f..9050e9a 100644 --- a/conf/systemd-agent.service +++ b/conf/systemd-agent.service @@ -5,17 +5,13 @@ Requires=network.target After=network.target [Service] -User=www-data -Environment="ARGOS_AGENT_TOKEN=Secret" -Environment="ARGOS_AGENT_SERVER_URL=http://127.0.0.1:8000" -WorkingDirectory=/var/www/argos/ -ExecStart=/var/www/argos/venv/bin/argos agent --max-tasks 20 --wait-time 10 --log-level DEBUG +User=argos +EnvironmentFile=/etc/default/argos-agent +WorkingDirectory=/opt/argos/ +ExecStart=/opt/argos/venv/bin/argos agent --max-tasks $ARGOS_AGENT_MAX_TASKS \ + --wait-time $ARGOS_AGENT_WAIT_TIME \ + --log-level $ARGOS_AGENT_LOGLEVEL SyslogIdentifier=argos-agent [Install] WantedBy=multi-user.target - -# NB: it may be better to -# - use a dedicated user -# - use a EnvironmentFile=/etc/default/argos-agent in order to enable configuration -# changes without doing a systemctl daemon-reload diff --git a/conf/systemd-server.service b/conf/systemd-server.service index 9326b39..b0c4fe4 100644 --- a/conf/systemd-server.service +++ b/conf/systemd-server.service @@ -6,19 +6,18 @@ After=network.target postgresql.service PartOf=postgresql.service [Service] -User=www-data -WorkingDirectory=/var/www/argos/ -Environment="ARGOS_SERVER_WORKERS=4" -Environment="ARGOS_SERVER_SOCKET=127.0.0.1:8000" -ExecStartPre=/var/www/argos/venv/bin/argos server migrate -ExecStartPre=/var/www/argos/venv/bin/argos server reload-config -ExecStart=/var/www/argos/venv/bin/gunicorn "argos.server.main:get_application()" -w $ARGOS_SERVER_WORKERS -k uvicorn.workers.UvicornWorker -b $ARGOS_SERVER_SOCKET -ExecReload=/var/www/argos/venv/bin/argos server reload +User=argos +WorkingDirectory=/opt/argos/ +EnvironmentFile=/etc/default/argos-server +ExecStartPre=/opt/argos/venv/bin/argos server migrate +ExecStartPre=/opt/argos/venv/bin/argos server reload-config +ExecStart=/opt/argos/venv/bin/gunicorn "argos.server.main:get_application()" \ + --workers $ARGOS_SERVER_WORKERS \ + --worker-class uvicorn.workers.UvicornWorker \ + --bind $ARGOS_SERVER_SOCKET \ + --forwarded-allow-ips $ARGOS_SERVER_FORWARDED_ALLOW_IPS +ExecReload=/opt/argos/venv/bin/argos server reload-config SyslogIdentifier=argos-server [Install] WantedBy=multi-user.target - -# NB: it may be better to -# - use a EnvironmentFile=/etc/default/argos-server in order to enable configuration -# changes without doing a systemctl daemon-reload diff --git a/docs/cli.md b/docs/cli.md index be7b0ec..16057bc 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -77,11 +77,12 @@ Options: --help Show this message and exit. Commands: - cleandb Clean the database (to run routinely) - generate-token Generate a token for agents - migrate Run database migrations - reload-config Load or reload tasks’ configuration - start Starts the server (use only for testing or development!) + cleandb Clean the database (to run routinely) + generate-config Output an example config file. + generate-token Generate a token for agents + migrate Run database migrations + reload-config Load or reload tasks’ configuration + start Starts the server (use only for testing or development!) ``` +### Server watch-agents + + + +```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. + --help Show this message and exit. +``` + + + ### Server reload-config + +### Server user management + +To access Argos’ web interface, you need to create at least one user. + +You can manage users only through CLI. + + + +```man +Usage: argos server user [OPTIONS] COMMAND [ARGS]... + +Options: + --help Show this message and exit. + +Commands: + add Add new user + change-password Change user’s password + delete Delete user + disable Disable user + enable Enable user + show List all users + verify-password Test user’s password +``` + + + +### Add user + + + +```man +Usage: argos server user add [OPTIONS] + + Add new user + +Options: + --config TEXT Path of the configuration file. If ARGOS_YAML_FILE + environment variable is set, its value will be used instead. + --name TEXT Name of the user to create. + --password TEXT + --help Show this message and exit. +``` + + + +### Change the password of a user + + + +```man +Usage: argos server user change-password [OPTIONS] + + Change user’s password + +Options: + --config TEXT Path of the configuration file. If ARGOS_YAML_FILE + environment variable is set, its value will be used instead. + --name TEXT Name of the user you want to change the password. + --password TEXT + --help Show this message and exit. +``` + + + +### Delete a user + + + +```man +Usage: argos server user delete [OPTIONS] + + Delete user + +Options: + --config TEXT Path of the configuration file. If ARGOS_YAML_FILE environment + variable is set, its value will be used instead. + --name TEXT Name of the user to delete. [required] + --help Show this message and exit. +``` + + + +### Disable a user + +Disabling a user prevents the user to login and access Argos’ web interface but its credentials are still stored in Argos’ database. + + + +```man +Usage: argos server user disable [OPTIONS] + + Disable user + +Options: + --config TEXT Path of the configuration file. If ARGOS_YAML_FILE environment + variable is set, its value will be used instead. + --name TEXT Name of the user to disable. [required] + --help Show this message and exit. +``` + + + +### Enable a user + +Enabling a user prevents the user to login and access Argos’ web interface. + +Obviously, the user needs to exists and to be disabled before using the command. + + + +```man +Usage: argos server user enable [OPTIONS] + + Enable user + +Options: + --config TEXT Path of the configuration file. If ARGOS_YAML_FILE environment + variable is set, its value will be used instead. + --name TEXT Name of the user to reenable [required] + --help Show this message and exit. +``` + + + +### List all users + +Show all accounts, with their status (enabled or disabled). + + + +```man +Usage: argos server user show [OPTIONS] + + List all users + +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. +``` + + + +### Test the password of a user + +You can verify that you have the right password for a user with the following command: + + + +```man +Usage: argos server user verify-password [OPTIONS] + + Test user’s password + +Options: + --config TEXT Path of the configuration file. If ARGOS_YAML_FILE + environment variable is set, its value will be used instead. + --name TEXT Name of the user you want to test the password for. + [required] + --password TEXT + --help Show this message and exit. +``` + + diff --git a/docs/deployment/nginx.md b/docs/deployment/nginx.md index 5f974d0..25af5be 100644 --- a/docs/deployment/nginx.md +++ b/docs/deployment/nginx.md @@ -1,13 +1,6 @@ # Using Nginx as reverse proxy -As Argos has no authentication mechanism for the front-end, you need to protect some routes with HTTP authentication. - -To do so on Debian, install `apache2-utils` then create a file containing the wanted credentials: -```bash -htpasswd -c /etc/nginx/argos.passwd argos_admin -``` - -You can then use this file to protect the front-end’s routes: +Here is a example for Nginx configuration: ```{literalinclude} ../../conf/nginx.conf --- caption: /etc/nginx/sites-available/argos.example.org diff --git a/pyproject.toml b/pyproject.toml index 29e1f41..7a6738b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,10 +22,13 @@ classifiers = [ dependencies = [ "alembic>=1.13.0,<1.14", + "bcrypt>=4.1.3,<5", "click>=8.1,<9", "fastapi>=0.103,<0.104", + "fastapi-login>=1.10.0,<2", "httpx>=0.25,<1", "Jinja2>=3.0,<4", + "passlib>=1.7.4,<2", "psycopg2-binary>=2.9,<3", "pydantic[email]>=2.4,<3", "pydantic-settings>=2.0,<3", @@ -96,3 +99,7 @@ testpaths = [ "argos" ] pythonpath = "." +filterwarnings = [ + "ignore:'crypt' is deprecated and slated for removal in Python 3.13:DeprecationWarning", + "ignore:The 'app' shortcut is now deprecated:DeprecationWarning", +] diff --git a/tests/config.yaml b/tests/config.yaml index 1242298..82f055b 100644 --- a/tests/config.yaml +++ b/tests/config.yaml @@ -3,6 +3,7 @@ general: # The database URL, as defined in SQLAlchemy docs : https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls url: "sqlite:////tmp/test-argos.db" env: test + cookie_secret: "foo-bar-baz" frequency: "1m" alerts: ok: diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..1225379 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,150 @@ +import os + +from click.testing import CliRunner +from argos.commands import ( + add, + verify_password, + change_password, + show, + disable, + enable, + delete, +) + + +os.environ["ARGOS_APP_ENV"] = "test" +os.environ["ARGOS_YAML_FILE"] = "tests/config.yaml" + + +def test_add_user(): + runner = CliRunner() + result = runner.invoke(add, ["--name", "foo"], input="bar\nbar\n") + assert result.exit_code == 0 + assert result.output == "Password: \nRepeat for confirmation: \nUser foo added.\n" + result = runner.invoke(add, ["--name", "foo"], input="bar\nbar\n") + assert result.exit_code == 1 + assert ( + result.output + == "Password: \nRepeat for confirmation: \nUser foo already exists.\n" + ) + result = runner.invoke(add, ["--name", "baz", "--password", "qux"]) + assert result.exit_code == 0 + assert result.output == "User baz added.\n" + + +def test_user_password(): + runner = CliRunner() + result = runner.invoke(verify_password, ["--name", "foo"], input="bar\n") + assert result.exit_code == 0 + assert result.output == "Password: \nThe provided password is correct.\n" + result = runner.invoke(verify_password, ["--name", "foo", "--password", "bar"]) + assert result.exit_code == 0 + assert result.output == "The provided password is correct.\n" + result = runner.invoke(verify_password, ["--name", "quux", "--password", "corge"]) + assert result.exit_code == 1 + assert result.output == "User quux does not exist.\n" + result = runner.invoke(verify_password, ["--name", "foo", "--password", "grault"]) + assert result.exit_code == 2 + assert result.output == "Wrong password!\n" + + +def test_change_password(): + runner = CliRunner() + result = runner.invoke(verify_password, ["--name", "foo", "--password", "grault"]) + assert result.exit_code == 2 + assert result.output == "Wrong password!\n" + result = runner.invoke(change_password, ["--name", "foo"], input="grault\ngrault\n") + assert result.exit_code == 0 + assert ( + result.output + == "Password: \nRepeat for confirmation: \nPassword of user foo changed.\n" + ) + result = runner.invoke(verify_password, ["--name", "foo", "--password", "grault"]) + assert result.exit_code == 0 + assert result.output == "The provided password is correct.\n" + result = runner.invoke(change_password, ["--name", "foo", "--password", "bar"]) + assert result.exit_code == 0 + assert result.output == "Password of user foo changed.\n" + result = runner.invoke(verify_password, ["--name", "foo", "--password", "bar"]) + assert result.exit_code == 0 + assert result.output == "The provided password is correct.\n" + result = runner.invoke(verify_password, ["--name", "quux", "--password", "bar"]) + assert result.exit_code == 1 + assert result.output == "User quux does not exist.\n" + + +def test_show(): + runner = CliRunner() + result = runner.invoke(show) + assert result.exit_code == 0 + assert ( + result.output + == "✅ means that the user is enabled.\n❌ means that the user is disabled.\n" + "✅ baz, last login: None\n✅ foo, last login: None\n" + ) + + +def test_disable(): + runner = CliRunner() + result = runner.invoke(disable, ["--name", "quux"]) + assert result.exit_code == 1 + assert result.output == "User quux does not exist.\n" + result = runner.invoke(disable, ["--name", "foo"]) + assert result.exit_code == 0 + assert result.output == "User foo disabled.\n" + result = runner.invoke(disable, ["--name", "foo"]) + assert result.exit_code == 2 + assert result.output == "User foo is already disabled.\n" + result = runner.invoke(show) + assert result.exit_code == 0 + assert ( + result.output + == "✅ means that the user is enabled.\n❌ means that the user is disabled.\n" + "✅ baz, last login: None\n❌ foo, last login: None\n" + ) + + +def test_enable(): + runner = CliRunner() + result = runner.invoke(enable, ["--name", "quux"]) + assert result.exit_code == 1 + assert result.output == "User quux does not exist.\n" + result = runner.invoke(enable, ["--name", "foo"]) + assert result.exit_code == 0 + assert result.output == "User foo enabled.\n" + result = runner.invoke(enable, ["--name", "foo"]) + assert result.exit_code == 2 + assert result.output == "User foo is already enabled.\n" + result = runner.invoke(show) + assert result.exit_code == 0 + assert ( + result.output + == "✅ means that the user is enabled.\n❌ means that the user is disabled.\n" + "✅ baz, last login: None\n✅ foo, last login: None\n" + ) + + +def test_delete(): + runner = CliRunner() + result = runner.invoke(delete, ["--name", "quux"]) + assert result.exit_code == 1 + assert result.output == "User quux does not exist.\n" + result = runner.invoke(delete, ["--name", "foo"]) + assert result.exit_code == 0 + assert result.output == "User foo deleted.\n" + result = runner.invoke(delete, ["--name", "foo"]) + assert result.exit_code == 1 + assert result.output == "User foo does not exist.\n" + result = runner.invoke(show) + assert result.exit_code == 0 + assert ( + result.output + == "✅ means that the user is enabled.\n❌ means that the user is disabled.\n" + "✅ baz, last login: None\n" + ) + result = runner.invoke(delete, ["--name", "baz"]) + assert result.exit_code == 0 + assert result.output == "User baz deleted.\n" + result = runner.invoke(show) + assert result.exit_code == 1 + assert result.output == "There is no users in database.\n" diff --git a/tests/test_queries.py b/tests/test_queries.py index 0300b23..c468362 100644 --- a/tests/test_queries.py +++ b/tests/test_queries.py @@ -4,18 +4,18 @@ import pytest from argos import schemas from argos.server import queries -from argos.server.models import Result, Task +from argos.server.models import Result, Task, User @pytest.mark.asyncio -async def test_remove_old_results(db, ten_tasks): - for task in ten_tasks: - for i in range(5): +async def test_remove_old_results(db, ten_tasks): # pylint: disable-msg=redefined-outer-name + for _task in ten_tasks: + for _ in range(5): result = Result( submitted_at=datetime.now(), status="success", context={"foo": "bar"}, - task=task, + task=_task, agent_id="test", severity="ok", ) @@ -28,8 +28,8 @@ async def test_remove_old_results(db, ten_tasks): deleted = await queries.remove_old_results(db, 2) assert deleted == 30 assert db.query(Result).count() == 20 - for task in ten_tasks: - assert db.query(Result).filter(Result.task == task).count() == 2 + for _task in ten_tasks: + assert db.query(Result).filter(Result.task == _task).count() == 2 @pytest.mark.asyncio @@ -40,7 +40,7 @@ async def test_remove_old_results_with_empty_db(db): @pytest.mark.asyncio -async def test_release_old_locks(db, ten_locked_tasks, ten_tasks): +async def test_release_old_locks(db, ten_locked_tasks, ten_tasks): # pylint: disable-msg=redefined-outer-name assert db.query(Task).count() == 20 released = await queries.release_old_locks(db, 10) assert released == 10 @@ -54,9 +54,9 @@ async def test_release_old_locks_with_empty_db(db): @pytest.mark.asyncio -async def test_update_from_config_with_duplicate_tasks(db, empty_config): +async def test_update_from_config_with_duplicate_tasks(db, empty_config): # pylint: disable-msg=redefined-outer-name # We pass the same path twice - fake_path = dict(path="/", checks=[{"body-contains": "foo"}]) + fake_path = {"path": "/", "checks": [{"body-contains": "foo"}]} website = schemas.config.Website( domain="https://example.org", paths=[ @@ -79,7 +79,9 @@ async def test_update_from_config_with_duplicate_tasks(db, empty_config): @pytest.mark.asyncio async def test_update_from_config_db_can_remove_duplicates_and_old_tasks( - db, empty_config, task + db, + empty_config, + task, # pylint: disable-msg=redefined-outer-name ): # Add a duplicate in the db same_task = Task( @@ -96,10 +98,11 @@ async def test_update_from_config_db_can_remove_duplicates_and_old_tasks( website = schemas.config.Website( domain=task.domain, paths=[ - dict( - path="https://another-example.com", checks=[{task.check: task.expected}] - ), - dict(path=task.url, checks=[{task.check: task.expected}]), + { + "path": "https://another-example.com", + "checks": [{task.check: task.expected}], + }, + {"path": task.url, "checks": [{task.check: task.expected}]}, ], ) empty_config.websites = [website] @@ -110,9 +113,10 @@ async def test_update_from_config_db_can_remove_duplicates_and_old_tasks( website = schemas.config.Website( domain=task.domain, paths=[ - dict( - path="https://another-example.com", checks=[{task.check: task.expected}] - ), + { + "path": "https://another-example.com", + "checks": [{task.check: task.expected}], + } ], ) empty_config.websites = [website] @@ -122,14 +126,12 @@ async def test_update_from_config_db_can_remove_duplicates_and_old_tasks( @pytest.mark.asyncio -async def test_update_from_config_db_updates_existing_tasks(db, empty_config, task): +async def test_update_from_config_db_updates_existing_tasks(db, empty_config, task): # pylint: disable-msg=redefined-outer-name assert db.query(Task).count() == 1 website = schemas.config.Website( domain=task.domain, - paths=[ - dict(path=task.url, checks=[{task.check: task.expected}]), - ], + paths=[{"path": task.url, "checks": [{task.check: task.expected}]}], ) empty_config.websites = [website] @@ -139,7 +141,11 @@ async def test_update_from_config_db_updates_existing_tasks(db, empty_config, ta @pytest.mark.asyncio async def test_reschedule_all( - db, ten_tasks, ten_warning_tasks, ten_critical_tasks, ten_ok_tasks + db, + ten_tasks, + ten_warning_tasks, + ten_critical_tasks, + ten_ok_tasks, # pylint: disable-msg=redefined-outer-name ): assert db.query(Task).count() == 40 assert db.query(Task).filter(Task.severity == "unknown").count() == 10 @@ -154,18 +160,65 @@ async def test_reschedule_all( assert db.query(Task).filter(Task.next_run <= one_hour_ago).count() == 30 +@pytest.mark.asyncio +async def test_add_user(db): + users = await queries.list_users(db) + assert users.count() == 0 + + _user = await queries.add_user(db, "john", "doe") + assert _user.username == "john" + assert _user.password == "doe" + assert _user.disabled == False + assert _user.created_at is not None + assert _user.updated_at is None + assert _user.last_login_at is None + + _user = await queries.get_user(db, "morgan") + assert _user is None + + _user = await queries.get_user(db, "john") + assert _user.username == "john" + assert _user.password == "doe" + assert _user.disabled == False + assert _user.created_at is not None + assert _user.updated_at is None + assert _user.last_login_at is None + + users = await queries.list_users(db) + assert users.count() == 1 + + +@pytest.mark.asyncio +async def test_remove_user(db, user): # pylint: disable-msg=redefined-outer-name + users = await queries.list_users(db) + assert users.count() == 1 + + assert user.username == "jane" + assert user.password == "doe" + assert user.disabled == False + assert user.created_at is not None + assert user.updated_at is None + assert user.last_login_at is None + + db.delete(user) + db.commit() + + users = await queries.list_users(db) + assert users.count() == 0 + + @pytest.fixture def task(db): - task = Task( + _task = Task( url="https://www.example.com", domain="https://www.example.com", check="body-contains", expected="foo", frequency=1, ) - db.add(task) + db.add(_task) db.commit() - return task + return _task @pytest.fixture @@ -173,6 +226,7 @@ def empty_config(): return schemas.config.Config( general=schemas.config.General( db=schemas.config.DbSettings(url="sqlite:////tmp/test-argos.db"), + cookie_secret="foo-bar-baz", frequency="1m", alerts=schemas.config.Alert( ok=["", ""], @@ -192,9 +246,9 @@ def empty_config(): @pytest.fixture -def ten_results(db, task): +def ten_results(db, task): # pylint: disable-msg=redefined-outer-name results = [] - for i in range(10): + for _ in range(10): result = Result( submitted_at=datetime.now(), status="success", @@ -213,8 +267,8 @@ def ten_results(db, task): def ten_locked_tasks(db): a_minute_ago = datetime.now() - timedelta(minutes=1) tasks = [] - for i in range(10): - task = Task( + for _ in range(10): + _task = Task( url="https://www.example.com", domain="example.com", check="body-contains", @@ -223,8 +277,8 @@ def ten_locked_tasks(db): selected_by="test", selected_at=a_minute_ago, ) - db.add(task) - tasks.append(task) + db.add(_task) + tasks.append(_task) db.commit() return tasks @@ -233,8 +287,8 @@ def ten_locked_tasks(db): def ten_tasks(db): now = datetime.now() tasks = [] - for i in range(10): - task = Task( + for _ in range(10): + _task = Task( url="https://www.example.com", domain="example.com", check="body-contains", @@ -243,8 +297,8 @@ def ten_tasks(db): selected_by="test", selected_at=now, ) - db.add(task) - tasks.append(task) + db.add(_task) + tasks.append(_task) db.commit() return tasks @@ -253,8 +307,8 @@ def ten_tasks(db): def ten_warning_tasks(db): now = datetime.now() tasks = [] - for i in range(10): - task = Task( + for _ in range(10): + _task = Task( url="https://www.example.com", domain="example.com", check="body-contains", @@ -263,8 +317,8 @@ def ten_warning_tasks(db): next_run=now, severity="warning", ) - db.add(task) - tasks.append(task) + db.add(_task) + tasks.append(_task) db.commit() return tasks @@ -273,8 +327,8 @@ def ten_warning_tasks(db): def ten_critical_tasks(db): now = datetime.now() tasks = [] - for i in range(10): - task = Task( + for _ in range(10): + _task = Task( url="https://www.example.com", domain="example.com", check="body-contains", @@ -283,8 +337,8 @@ def ten_critical_tasks(db): next_run=now, severity="critical", ) - db.add(task) - tasks.append(task) + db.add(_task) + tasks.append(_task) db.commit() return tasks @@ -293,8 +347,8 @@ def ten_critical_tasks(db): def ten_ok_tasks(db): now = datetime.now() tasks = [] - for i in range(10): - task = Task( + for _ in range(10): + _task = Task( url="https://www.example.com", domain="example.com", check="body-contains", @@ -303,7 +357,19 @@ def ten_ok_tasks(db): next_run=now, severity="ok", ) - db.add(task) - tasks.append(task) + db.add(_task) + tasks.append(_task) db.commit() return tasks + + +@pytest.fixture +def user(db): + _user = User( + username="jane", + password="doe", + disabled=False, + ) + db.add(_user) + db.commit() + return _user From 1b073376e6ffb3991fba1f454cec05190e048ba5 Mon Sep 17 00:00:00 2001 From: Luc Didry Date: Fri, 21 Jun 2024 14:52:35 +0200 Subject: [PATCH 20/22] =?UTF-8?q?=F0=9F=A9=B9=20=E2=80=94=20Fix=20small=20?= =?UTF-8?q?link=20bug=20in=20notifications?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- argos/server/alerting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/argos/server/alerting.py b/argos/server/alerting.py index 173b4b1..bb833a2 100644 --- a/argos/server/alerting.py +++ b/argos/server/alerting.py @@ -52,7 +52,7 @@ Status: {severity} Time: {result.submitted_at} 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} """ @@ -109,7 +109,7 @@ Status: {severity} Time: {result.submitted_at} 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} """ From b85d7e90a8890306a8d32142f55be3dbd48625f0 Mon Sep 17 00:00:00 2001 From: Luc Didry Date: Mon, 24 Jun 2024 16:12:50 +0200 Subject: [PATCH 21/22] =?UTF-8?q?=F0=9F=93=9D=20=E2=80=94=20Improve=20docu?= =?UTF-8?q?mentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- argos/commands.py | 9 +-- argos/config-example.yaml | 84 +++++++++++++++------------- argos/server/main.py | 4 ++ docs/cli.md | 54 +++++++++++++----- docs/configuration.md | 3 +- docs/installation/getting-started.md | 40 ++++++------- docs/installation/tl-dr.md | 7 ++- 7 files changed, 121 insertions(+), 80 deletions(-) diff --git a/argos/commands.py b/argos/commands.py index 9eb9700..0fc1491 100644 --- a/argos/commands.py +++ b/argos/commands.py @@ -61,16 +61,17 @@ def cli(): @cli.group() def server(): - pass + """Commands for managing server, server’s configuration and users""" @server.group() def user(): - pass + """User management""" @cli.command() def version(): + """Prints Argos’ version and exits""" click.echo(VERSION) @@ -93,7 +94,7 @@ def version(): type=click.Choice(logging.LOG_LEVELS, case_sensitive=False), ) def agent(server_url, auth, max_tasks, wait_time, log_level): - """Get and run tasks to the provided server. Will wait for new tasks. + """Get and run tasks for the provided server. Will wait for new tasks. Usage: argos agent https://argos.example.org "auth-token-here" @@ -536,7 +537,7 @@ async def generate_token(): @server.command() @coroutine async def generate_config(): - """Output an example config file. + """Output a self-documented example config file. \b Redirect the output to a file to save it: diff --git a/argos/config-example.yaml b/argos/config-example.yaml index aed2c10..41f00d6 100644 --- a/argos/config-example.yaml +++ b/argos/config-example.yaml @@ -13,9 +13,13 @@ general: # to get a good string for cookie_secret, run: # openssl rand -hex 32 cookie_secret: "foo_bar_baz" - frequency: "1m" # Run checks every minute. + # Default delay for checks. + # Can be superseeded in domain configuration. + # For ex., to run checks every minute: + 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. alerts: ok: - local @@ -25,23 +29,26 @@ general: - local unknown: - 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 + # Mail configuration is quite straight-forward + # 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 + # Create an app on your Gotify server and put its token here + # See https://gotify.net/ for details about Gotify + # gotify: + # - url: https://example.org + # tokens: + # - foo + # - bar service: secrets: @@ -56,25 +63,26 @@ ssl: # It's also possible to define the checks in another file # with the include syntax: -# -# websites: !include websites.yaml +# +# websites: !include websites.yaml # websites: - - domain: "https://mypads.example.org" - paths: - - path: "/mypads/" - checks: - - status-is: 200 - - body-contains: '
' - - ssl-certificate-expiration: "on-check" - - path: "/admin/" - checks: - - status-is: 401 - - domain: "https://munin.example.org" - paths: - - path: "/" - checks: - - status-is: 301 - - path: "/munin/" - checks: - - status-is: 401 + - domain: "https://mypads.example.org" + paths: + - path: "/mypads/" + checks: + - status-is: 200 + - body-contains: '
' + - ssl-certificate-expiration: "on-check" + - path: "/admin/" + checks: + - status-is: 401 + - domain: "https://munin.example.org" + frequency: "20m" + paths: + - path: "/" + checks: + - status-is: 301 + - path: "/munin/" + checks: + - status-is: 401 diff --git a/argos/server/main.py b/argos/server/main.py index d1f3b5f..898b202 100644 --- a/argos/server/main.py +++ b/argos/server/main.py @@ -93,6 +93,10 @@ def setup_database(appli): def create_manager(cookie_secret): + if cookie_secret == "foo_bar_baz": + logger.warning( + "You should change the cookie_secret secret in your configuration file." + ) return LoginManager( cookie_secret, "/login", diff --git a/docs/cli.md b/docs/cli.md index 2d934ff..83a98f3 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -26,9 +26,9 @@ Options: --help Show this message and exit. Commands: - agent Get and run tasks to the provided server. - server - version + agent Get and run tasks for the provided server. + server Commands for managing server, server’s configuration and users + version Prints Argos’ version and exits ``` -### Server generate-token command +### Server generate-config + + + +```man +Usage: argos server generate-config [OPTIONS] + + Output a self-documented example config file. + + Redirect the output to a file to save it: + argos server generate-config > /etc/argos/config.yaml + +Options: + --help Show this message and exit. +``` + + + +### Server generate-token -### Add user +#### Add user -### Change the password of a user +#### Change the password of a user -### Delete a user +#### Delete a user -### Disable a user +#### Disable a user Disabling a user prevents the user to login and access Argos’ web interface but its credentials are still stored in Argos’ database. @@ -365,7 +391,7 @@ Options: -### Enable a user +#### Enable a user Enabling a user prevents the user to login and access Argos’ web interface. @@ -391,7 +417,7 @@ Options: -### List all users +#### List all users Show all accounts, with their status (enabled or disabled). @@ -414,7 +440,7 @@ Options: -### Test the password of a user +#### Test the password of a user You can verify that you have the right password for a user with the following command: diff --git a/docs/configuration.md b/docs/configuration.md index 4c819a4..576c55c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -2,8 +2,7 @@ Argos uses a simple YAML configuration file to define the server’s configuration, the websites to monitor and the checks to run on these websites. -Here is a simple configuration file: - +Here is a simple self-documented configuration file, which you can get with [`argos server generate-config`](cli.md#server-generate-config): ```{literalinclude} ../conf/config-example.yaml --- diff --git a/docs/installation/getting-started.md b/docs/installation/getting-started.md index b9876eb..65fae33 100644 --- a/docs/installation/getting-started.md +++ b/docs/installation/getting-started.md @@ -78,7 +78,7 @@ argos server generate-config > /etc/argos/config.yaml chmod 600 /etc/argos/config.yaml ``` -Please note that the only supported database engines are SQLite for development and PostgreSQL for production. +Please note that the only supported database engines are SQLite for development and [PostgreSQL](postgresql.md) for production. ## Apply migrations to database @@ -98,6 +98,23 @@ Populate the database with the tasks: argos server reload-config ``` +## Generating a token + +The agent needs an authentication token to be able to communicate with the server. + +You can generate an authentication token with the following command: +```bash +argos server generate-token +``` + +Add the token in the configuration file, in the following setting: + +```yaml +service: + secrets: + - "auth-token" +``` + ## Starting the server Then you can start the server: @@ -142,23 +159,6 @@ See (but Gunico See [here](../deployment/systemd.md#server) for a systemd service example and [here](../deployment/nginx.md) for a nginx configuration example. -## Generating a token - -The agent needs an authentication token to be able to communicate with the server. - -You can generate an authentication token with the following command: -```bash -argos server generate-token -``` - -Add the token in the configuration file, in the following setting: - -```yaml -service: - secrets: - - "auth-token" -``` - ## Running the agent You can run the agent on the same machine as the server, or on a different machine. @@ -170,7 +170,7 @@ argos agent http://localhost:8000 "auth-token" ## Cleaning the database -You also have to run cleaning tasks periodically. `argos server clean --help` will give you more information on how to do that. +You have to run cleaning task periodically. `argos server cleandb --help` will give you more information on how to do that. Here is a crontab example, which will clean the db each hour: @@ -182,7 +182,7 @@ Here is a crontab example, which will clean the db each hour: ## Watch the agents -In order to be sure that agents are up and communicate with the server, you can run periodically the `argos server watch-agents` command. +In order to be sure that agents are up and communicate with the server, you can periodically run the `argos server watch-agents` command. Here is a crontab example, which will check the agents every 5 minutes: diff --git a/docs/installation/tl-dr.md b/docs/installation/tl-dr.md index 88c1981..26b35b6 100644 --- a/docs/installation/tl-dr.md +++ b/docs/installation/tl-dr.md @@ -14,7 +14,7 @@ python3 -m venv venv source venv/bin/activate pip install argos-monitoring argos server generate-config | - sed -e "s@production@test@" \ + sed -e "s@production@dev@" \ -e "s@url: .postgresql.*@url: \"sqlite:////tmp/argos.db\"@" > argos-config.yaml argos server migrate ARGOS_TOKEN=$(argos server generate-token) @@ -149,7 +149,10 @@ systemctl status argos-server.service argos-agent.service If all works well, you have to put some cron tasks in `argos` crontab: ```bash -echo -e "*/10 * * * * /opt/argos/venv/bin/argos server cleandb --max-lock-seconds 120 --max-results 1200\n*/10 * * * * /opt/argos/venv/bin/argos server watch-agents --time-without-agent 10" | crontab -u argos - +cat < Date: Mon, 24 Jun 2024 16:22:13 +0200 Subject: [PATCH 22/22] =?UTF-8?q?=F0=9F=93=9D=20=E2=80=94=20Improve=20rele?= =?UTF-8?q?ase=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/developer/release.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/developer/release.md b/docs/developer/release.md index ee6af92..0a3500e 100644 --- a/docs/developer/release.md +++ b/docs/developer/release.md @@ -17,6 +17,8 @@ You'll need to get an account on [PyPI](https://pypi.org), where the packages wi Here is the quick version. If you need more details, some parts are explained in more details in the next sections. ```bash +# Be sure you are on the good branch +git checkout main # Ensure the tests run correctly make test