💥 — Remove env vars and only use the configuration file (fix #47)

This commit is contained in:
Luc Didry 2024-06-19 13:27:39 +02:00
parent 907cd5878f
commit 638dcc0295
No known key found for this signature in database
GPG key ID: EA868E12D0257E3C
16 changed files with 111 additions and 172 deletions

View file

@ -10,6 +10,7 @@
- ✨ — Add command to warn if its been long since last viewing an agent (fix #49) - ✨ — Add command to warn if its been long since last viewing an agent (fix #49)
- 💥 — Change default config file path to argos-config.yaml (fix #36) - 💥 — Change default config file path to argos-config.yaml (fix #36)
- 📝 — New documentation URL: doc is now on https://argos-monitoring.framasoft.org/ - 📝 — New documentation URL: doc is now on https://argos-monitoring.framasoft.org/
- 💥 — Remove env vars and only use the configuration file
## 0.1.1 ## 0.1.1

View file

@ -243,15 +243,12 @@ async def reload_config(config):
# The imports are made here otherwise the agent will need server configuration files. # The imports are made here otherwise the agent will need server configuration files.
from argos.server import queries from argos.server import queries
from argos.server.main import get_application, read_config from argos.server.main import read_config
from argos.server.settings import get_app_settings
appli = get_application() _config = read_config(config)
settings = get_app_settings()
config = read_config(appli, settings)
db = await get_db() db = await get_db()
changed = await queries.update_from_config(db, config) changed = await queries.update_from_config(db, _config)
click.echo(f"{changed['added']} tasks added") click.echo(f"{changed['added']} tasks added")
click.echo(f"{changed['vanished']} tasks deleted") click.echo(f"{changed['vanished']} tasks deleted")
@ -274,13 +271,13 @@ async def migrate(config):
os.environ["ARGOS_YAML_FILE"] = config os.environ["ARGOS_YAML_FILE"] = config
# The imports are made here otherwise the agent will need server configuration files. # The imports are made here otherwise the agent will need server configuration files.
from argos.server.settings import get_app_settings from argos.server.settings import read_yaml_config
settings = get_app_settings() settings = read_yaml_config(config)
current_dir = Path(__file__).resolve().parent current_dir = Path(__file__).resolve().parent
alembic_cfg = Config(current_dir / "server" / "migrations" / "alembic.ini") alembic_cfg = Config(current_dir / "server" / "migrations" / "alembic.ini")
alembic_cfg.set_main_option("sqlalchemy.url", settings.database_url) alembic_cfg.set_main_option("sqlalchemy.url", str(settings.general.db.url))
command.upgrade(alembic_cfg, "head") command.upgrade(alembic_cfg, "head")

View file

@ -1,4 +1,15 @@
general: general:
db:
# The database URL, as defined in SQLAlchemy docs : https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls
# Example for SQLite: "sqlite:////tmp/argos.db"
url: "postgresql://argos:argos@localhost/argos"
# You configure the size of the database pool of connection, and the max overflow (until when new connections are accepted ?)
# See https://docs.sqlalchemy.org/en/20/core/pooling.html#sqlalchemy.pool.QueuePool.params.pool_size for details
pool_size: 10
max_overflow: 20
# Can be "production", "dev", "test".
# If not present, default value is "production"
env: "production"
frequency: "1m" # Run checks every minute. frequency: "1m" # Run checks every minute.
# Which way do you want to be warned when a check goes to that severity? # Which way do you want to be warned when a check goes to that severity?
# "local" emits a message in the server log # "local" emits a message in the server log

View file

@ -3,7 +3,7 @@ import logging
LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
# Print level before message # Print level before message
logging.basicConfig(format="%(levelname)s: %(message)s") logging.basicConfig(format="%(levelname)-9s %(message)s")
# XXX We probably want different loggers for client and server. # XXX We probably want different loggers for client and server.
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -8,17 +8,27 @@ from pydantic import (
BaseModel, BaseModel,
ConfigDict, ConfigDict,
HttpUrl, HttpUrl,
PostgresDsn,
StrictBool, StrictBool,
EmailStr, EmailStr,
PositiveInt, PositiveInt,
field_validator, field_validator,
) )
from pydantic.functional_validators import BeforeValidator from pydantic.functional_validators import BeforeValidator
from pydantic.networks import UrlConstraints
from pydantic_core import Url
from typing_extensions import Annotated from typing_extensions import Annotated
from argos.schemas.utils import string_to_duration from argos.schemas.utils import string_to_duration
Severity = Literal["warning", "error", "critical", "unknown"] Severity = Literal["warning", "error", "critical", "unknown"]
Environment = Literal["dev", "test", "production"]
SQLiteDsn = Annotated[
Url,
UrlConstraints(
allowed_schemes=["sqlite"],
),
]
def parse_threshold(value): def parse_threshold(value):
@ -134,10 +144,18 @@ class GotifyUrl(BaseModel):
tokens: List[str] tokens: List[str]
class DbSettings(BaseModel):
url: PostgresDsn | SQLiteDsn
pool_size: int = 10
max_overflow: int = 20
class General(BaseModel): class General(BaseModel):
"""Frequency for the checks and alerts""" """Frequency for the checks and alerts"""
frequency: int frequency: int
db: DbSettings
env: Environment = "production"
alerts: Alert alerts: Alert
mail: Optional[Mail] = None mail: Optional[Mail] = None
gotify: Optional[List[GotifyUrl]] = None gotify: Optional[List[GotifyUrl]] = None

View file

@ -1,3 +1,4 @@
import os
import sys import sys
from pathlib import Path from pathlib import Path
@ -9,20 +10,18 @@ from sqlalchemy.orm import sessionmaker
from argos.logging import logger from argos.logging import logger
from argos.server import models, routes, queries from argos.server import models, routes, queries
from argos.server.settings import get_app_settings, read_yaml_config from argos.server.settings import read_yaml_config
def get_application() -> FastAPI: def get_application() -> FastAPI:
"""Spawn Argos FastAPI server""" """Spawn Argos FastAPI server"""
settings = get_app_settings()
appli = FastAPI() appli = FastAPI()
config_file = os.environ["ARGOS_YAML_FILE"]
config = read_config(appli, settings) config = read_config(config_file)
# Settings is the pydantic settings object
# Config is the argos config object (built from yaml) # Config is the argos config object (built from yaml)
appli.state.config = config appli.state.config = config
appli.state.settings = settings
appli.add_event_handler( appli.add_event_handler(
"startup", "startup",
@ -79,10 +78,9 @@ def create_stop_app_handler(appli):
return stop_app return stop_app
def read_config(appli, settings): def read_config(yaml_file):
try: try:
config = read_yaml_config(settings.yaml_file) config = read_yaml_config(yaml_file)
appli.state.config = config
return config return config
except ValidationError as err: except ValidationError as err:
logger.error("Errors where found while reading configuration:") logger.error("Errors where found while reading configuration:")
@ -92,25 +90,26 @@ def read_config(appli, settings):
def setup_database(appli): def setup_database(appli):
settings = appli.state.settings config = appli.state.config
db_url = str(config.general.db.url)
logger.debug("Using database URL %s", db_url)
# For sqlite, we need to add connect_args={"check_same_thread": False} # For sqlite, we need to add connect_args={"check_same_thread": False}
logger.debug("Using database URL %s", settings.database_url) if config.general.env == "production" and db_url.startswith("sqlite:////tmp"):
if settings.database_url.startswith("sqlite:////tmp"):
logger.warning("Using sqlite in /tmp is not recommended for production") logger.warning("Using sqlite in /tmp is not recommended for production")
extra_settings = {} extra_settings = {}
if settings.db_pool_size: if config.general.db.pool_size:
extra_settings.setdefault("pool_size", settings.db_pool_size) extra_settings.setdefault("pool_size", config.general.db.pool_size)
if settings.db_max_overflow: if config.general.db.max_overflow:
extra_settings.setdefault("max_overflow", settings.db_max_overflow) extra_settings.setdefault("max_overflow", config.general.db.max_overflow)
engine = create_engine(settings.database_url, **extra_settings) engine = create_engine(db_url, **extra_settings)
def _fk_pragma_on_connect(dbapi_con, con_record): def _fk_pragma_on_connect(dbapi_con, con_record):
dbapi_con.execute("pragma foreign_keys=ON") dbapi_con.execute("pragma foreign_keys=ON")
if settings.database_url.startswith("sqlite:////"): if db_url.startswith("sqlite:///"):
event.listen(engine, "connect", _fk_pragma_on_connect) event.listen(engine, "connect", _fk_pragma_on_connect)
appli.state.SessionLocal = sessionmaker( appli.state.SessionLocal = sessionmaker(

View file

@ -1,75 +1,12 @@
"""Pydantic schemas for server""" """Pydantic schemas for server"""
from functools import lru_cache
from os import environ
from pathlib import Path from pathlib import Path
from typing import Optional, Union
import yaml import yaml
from pydantic_settings import BaseSettings, SettingsConfigDict
from yamlinclude import YamlIncludeConstructor from yamlinclude import YamlIncludeConstructor
from argos.schemas.config import Config from argos.schemas.config import Config
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="argos_", env_file=".env")
app_env: str
database_url: str
yaml_file: str
db_pool_size: Optional[int]
db_max_overflow: Optional[int]
class DevSettings(Settings):
"""Settings for dev environment.
Uses argos-config.yaml as config file.
Uses a SQLite database."""
app_env: str = "dev"
db_pool_size: Optional[int] = None
db_max_overflow: Optional[int] = None
database_url: str = "sqlite:////tmp/argos.db"
class TestSettings(Settings):
"""Settings for test environment.
Uses tests/config.yaml as config file.
Uses a SQLite database."""
app_env: str = "test"
yaml_file: str = "tests/config.yaml"
database_url: str = "sqlite:////tmp/test-argos.db"
db_pool_size: Optional[int] = None
db_max_overflow: Optional[int] = None
class ProdSettings(Settings):
"""Settings for prod environment."""
app_env: str = "prod"
db_pool_size: Optional[int] = 10
db_max_overflow: Optional[int] = 20
environments = {
"dev": DevSettings,
"prod": ProdSettings,
"test": TestSettings,
}
@lru_cache()
def get_app_settings() -> Union[None, Settings]:
"""Load settings depending on the environment"""
app_env = environ.get("ARGOS_APP_ENV", "dev")
settings = environments.get(app_env)
if settings is not None:
return settings()
return None
def read_yaml_config(filename): def read_yaml_config(filename):
parsed = _load_yaml(filename) parsed = _load_yaml(filename)
return Config(**parsed) return Config(**parsed)

View file

@ -1,5 +1,4 @@
ARGOS_YAML_FILE="/etc/argos/config.yaml" ARGOS_YAML_FILE="/etc/argos/config.yaml"
ARGOS_DATABASE_URL="postgresql://argos:THE_DB_PASSWORD@localhost/argos"
ARGOS_SERVER_WORKERS=4 ARGOS_SERVER_WORKERS=4
ARGOS_SERVER_SOCKET=127.0.0.1:8000 ARGOS_SERVER_SOCKET=127.0.0.1:8000
# Comma separated list of IP addresses of the web proxy (usually Nginx) # Comma separated list of IP addresses of the web proxy (usually Nginx)

View file

@ -191,7 +191,8 @@ Options:
checks have a timeout value of 60 seconds) checks have a timeout value of 60 seconds)
--config TEXT Path of the configuration file. If ARGOS_YAML_FILE --config TEXT Path of the configuration file. If ARGOS_YAML_FILE
environment variable is set, its value will be environment variable is set, its value will be
used instead. used instead. Default value: argos-config.yaml and
/etc/argos/config.yaml as fallback.
--help Show this message and exit. --help Show this message and exit.
``` ```

View file

@ -5,6 +5,7 @@
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
# pylint: disable-msg=invalid-name,redefined-builtin
import argos import argos
project = "Argos" project = "Argos"

View file

@ -1,51 +1,6 @@
# Configuration # Configuration
There are actually two configuration files: one for the service and one for the checks. Argos uses a simple YAML configuration file to define the servers configuration, the websites to monitor and the checks to run on these websites.
## Server configuration
The server configuration is done using environment variables. You can put them in a `.env` file at the root of the project.
Here is a list of the useful variables, in the `.env` format:
```{literalinclude} ../conf/.env.example
---
caption: .env
---
```
### Environment variables
Here are the environment variables you can define to configure how the service will behave :
#### ARGOS_YAML_FILE
The path to the yaml configuration file, defining the checks.
If you do not provide the environment variable, Argos will try to read `argos-config.yaml` in the current directory, then `/etc/config/argos.yaml`.
#### ARGOS_DATABASE_URL
The database url, as defined [in SQLAlchemy docs](https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls).
For instance, to connect to a postgres database on localhost with user, pass and dbname "argos":
```
ARGOS_DATABASE_URL="postgresql://argos:argos@localhost/argos"
```
#### DB_POOL_SIZE
#### DB_MAX_OVERFLOW
You configure the size of the database pool of connection, and the max overflow (until when new connections are accepted ?) These are documented [in the SQLAlchemy docs in greater details](https://docs.sqlalchemy.org/en/20/core/pooling.html#sqlalchemy.pool.QueuePool.params.pool_size)
```bash
DB_POOL_SIZE=10
DB_MAX_OVERFLOW=20
```
## Argos "checks" configuration
Argos uses a YAML configuration file to define the websites to monitor and the checks to run on these websites.
Here is a simple configuration file: Here is a simple configuration file:
@ -54,5 +9,4 @@ Here is a simple configuration file:
--- ---
caption: argos-config.yaml caption: argos-config.yaml
--- ---
``` ```

View file

@ -7,6 +7,21 @@ NB: if you want a quick-installation guide, we [got you covered](tl-dr.md).
- Python 3.11+ - Python 3.11+
- PostgreSQL 13+ (for production) - PostgreSQL 13+ (for production)
## Recommendation
Create a dedicated user for argos:
```bash
adduser --home /opt/argos --disabled-login --disabled-password --system argos
```
Do all the manipulations below in `/opt/argos/`, with the user `argos`.
Either use `sudo` or login as `argos` with the following command:
```bash
su argos -s /bin/bash
```
## Install with pip ## Install with pip
```bash ```bash
@ -24,7 +39,7 @@ pip install argos-monitoring
For production, we recommend the use of [Gunicorn](https://gunicorn.org/), which you can install at the same time as Argos: For production, we recommend the use of [Gunicorn](https://gunicorn.org/), which you can install at the same time as Argos:
```bash ```bash
pip install argos-monitoring[gunicorn] pip install "argos-monitoring[gunicorn]"
``` ```
## Install from sources ## Install from sources
@ -37,6 +52,8 @@ source venv/bin/activate
pip install -e . pip install -e .
``` ```
To install gunicorn, use `pip install -e ".[gunicorn]"` instead of `pip install -e .`
## Configure ## Configure
The quickest way to get started is to generate the configuration file from argos and edit it: The quickest way to get started is to generate the configuration file from argos and edit it:
@ -47,14 +64,18 @@ argos server generate-config > argos-config.yaml
You can read more about the configuration in the [configuration section](../configuration.md). You can read more about the configuration in the [configuration section](../configuration.md).
### Configure the server For production, we suggest to put your config in `/etc/argos/config.yaml` and restricts the files permissions.
As root:
```bash
mkdir /etc/argos
chown argos: /etc/argos
chmod 700 /etc/argos
```
Environment variables are used to configure the server. You can also put them in an `.env` file: Then, as `argos`:
```bash
```{literalinclude} ../../conf/.env.example argos server generate-config > /etc/argos/config.yaml
--- chmod 600 /etc/argos/config.yaml
caption: .env
---
``` ```
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 for production.
@ -69,7 +90,7 @@ argos server migrate
## Inject tasks into the database ## Inject tasks into the database
Argos keeps tasks configuration in database, take from the config file. Argos keeps tasks configuration in database, taken from the config file.
Populate the database with the tasks: Populate the database with the tasks:
@ -85,8 +106,6 @@ Then you can start the server:
argos server start argos server start
``` ```
The server reads the `yaml` file at startup, and populates the tasks queue with the checks defined in the configuration.
This way to start the server is not suitable for production, use it only for developing or testing. This way to start the server is not suitable for production, use it only for developing or testing.
## Starting the server for production ## Starting the server for production
@ -96,7 +115,7 @@ For production, you can use [Gunicorn](https://gunicorn.org/) to start the serve
To install Gunicorn in the virtualenv, if you didnt already install Argos that way: To install Gunicorn in the virtualenv, if you didnt already install Argos that way:
```bash ```bash
pip install argos-monitoring[gunicorn] pip install "argos-monitoring[gunicorn]"
``` ```
To start the server: To start the server:
@ -121,6 +140,8 @@ Gunicorn has a lot of other options, have a look at `gunicorn --help`.
Argos uses FastAPI, so you can use other ways to start the server. Argos uses FastAPI, so you can use other ways to start the server.
See <https://fastapi.tiangolo.com/deployment/manually/#asgi-servers> (but Gunicorn is recommended). See <https://fastapi.tiangolo.com/deployment/manually/#asgi-servers> (but Gunicorn is recommended).
See [here](../deployment/systemd.md#server) for a systemd service example and [here](../deployment/nginx.md) for a nginx configuration example.
## Generating a token ## Generating a token
The agent needs an authentication token to be able to communicate with the server. The agent needs an authentication token to be able to communicate with the server.
@ -141,7 +162,7 @@ service:
## Running the agent ## Running the agent
You can run the agent on the same machine as the server, or on a different machine. You can run the agent on the same machine as the server, or on a different machine.
The only requirement is that the agent can reach the server. The only requirement is that the agent can reach the server through HTTP or HTTPS.
```bash ```bash
argos agent http://localhost:8000 "auth-token" argos agent http://localhost:8000 "auth-token"

View file

@ -13,13 +13,17 @@ cd /tmp/argos
python3 -m venv venv python3 -m venv venv
source venv/bin/activate source venv/bin/activate
pip install argos-monitoring pip install argos-monitoring
argos server generate-config > argos-config.yaml argos server generate-config |
sed -e "s@production@test@" \
-e "s@url: .postgresql.*@url: \"sqlite:////tmp/argos.db\"@" > argos-config.yaml
argos server migrate argos server migrate
argos server generate-token ARGOS_TOKEN=$(argos server generate-token)
sed -e "s@# - secret_token@- $ARGOS_TOKEN@" -i argos-config.yaml
echo "The agent token is $ARGOS_TOKEN"
``` ```
Edit `argos-config.yaml`. Edit `argos-config.yaml`.
Add the generated token in it, as long as some real web sites to test. Add some real web sites to test.
Then: Then:
@ -48,16 +52,14 @@ sudo -u postgres psql -c "ALTER DATABASE argos SET TIMEZONE TO 'UTC';"
adduser --home /opt/argos --disabled-login --disabled-password --system argos adduser --home /opt/argos --disabled-login --disabled-password --system argos
cd /opt/argos cd /opt/argos
python3 -m venv venv sudo -u argos python3 -m venv venv
chown argos: -R venv sudo -u argos bash -c 'source venv/bin/activate && pip install "argos-monitoring[gunicorn]"'
sudo -u argos bash -c "source venv/bin/activate && pip install argos-monitoring[gunicorn]"
mkdir /etc/argos mkdir /etc/argos
/opt/argos/venv/bin/argos server generate-config > /etc/argos/config.yaml /opt/argos/venv/bin/argos server generate-config > /etc/argos/config.yaml
cat <<EOF > /etc/default/argos-server cat <<EOF > /etc/default/argos-server
ARGOS_YAML_FILE="/etc/argos/config.yaml" ARGOS_YAML_FILE="/etc/argos/config.yaml"
ARGOS_DATABASE_URL="postgresql://argos:THE_DB_PASSWORD@localhost/argos"
ARGOS_SERVER_WORKERS=4 ARGOS_SERVER_WORKERS=4
ARGOS_SERVER_SOCKET=127.0.0.1:8000 ARGOS_SERVER_SOCKET=127.0.0.1:8000
# Comma separated list of IP addresses of the web proxy (usually Nginx) # Comma separated list of IP addresses of the web proxy (usually Nginx)
@ -119,21 +121,18 @@ WantedBy=multi-user.target
EOF EOF
chown -R argos: /etc/default/argos-* /etc/argos/ chown -R argos: /etc/default/argos-* /etc/argos/
chmod 700 /etc/argos
chmod 600 /etc/argos/config.yaml
systemctl daemon-reload systemctl daemon-reload
``` ```
Then, edit `/etc/default/argos-server` to put your database password in it and change the other settings to suit your needs. Then, edit `/etc/argos/config.yaml` to put your database password in it and change the other settings to suit your needs.
Create a token for your agent : Create a token for your agent :
```bash ```bash
su - argos -s /bin/bash sudo -u argos /opt/argos/venv/bin/argos server generate-token
. /etc/default/argos-server
export ARGOS_DATABASE_URL
export ARGOS_YAML_FILE
/opt/argos/venv/bin/argos server generate-token
exit
``` ```
Edit `/etc/default/argos-agent` to put the generated token in it and change the other settings to suit your needs. Edit `/etc/default/argos-agent` to put the generated token in it and change the other settings to suit your needs.
@ -150,7 +149,7 @@ systemctl status argos-server.service argos-agent.service
If all works well, you have to put some cron tasks in `argos` crontab: If all works well, you have to put some cron tasks in `argos` crontab:
```bash ```bash
echo -e "*/10 * * * * . /etc/default/argos-server && export ARGOS_DATABASE_URL && export ARGOS_YAML_FILE && cd /opt/argos/ && /opt/argos/venv/bin/argos server cleandb --max-lock-seconds 120 --max-results 1200\n*/10 * * * * . /etc/default/argos-server && export ARGOS_DATABASE_URL && export ARGOS_YAML_FILE && cd /opt/argos/ && /opt/argos/venv/bin/argos server watch-agents --time-without-agent 10" | crontab -u argos - 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 -
``` ```
See the [this page](../deployment/nginx.md) for using Nginx as reverse proxy. See the [this page](../deployment/nginx.md) for using Nginx as reverse proxy.

View file

@ -1,4 +1,8 @@
general: general:
db:
# 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
frequency: "1m" frequency: "1m"
alerts: alerts:
ok: ok:

View file

@ -6,7 +6,7 @@ from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
os.environ["ARGOS_APP_ENV"] = "test" os.environ["ARGOS_YAML_FILE"] = "tests/config.yaml"
@pytest.fixture @pytest.fixture
@ -45,10 +45,6 @@ def _create_app() -> FastAPI:
) )
app = get_application() app = get_application()
# Hardcode the database url and the yaml file for testing purpose
# Otherwise, the app will try to read the .env file or the environment variables
app.state.settings.database_url = "sqlite:////tmp/test-argos.db"
app.state.settings.yaml_file = "tests/config.yaml"
setup_database(app) setup_database(app)
asyncio.run(connect_to_db(app)) asyncio.run(connect_to_db(app))

View file

@ -172,6 +172,7 @@ def task(db):
def empty_config(): def empty_config():
return schemas.config.Config( return schemas.config.Config(
general=schemas.config.General( general=schemas.config.General(
db=schemas.config.DbSettings(url="sqlite:////tmp/test-argos.db"),
frequency="1m", frequency="1m",
alerts=schemas.config.Alert( alerts=schemas.config.Alert(
ok=["", ""], ok=["", ""],