mirror of
https://framagit.org/framasoft/framaspace/argos.git
synced 2025-04-28 18:02:41 +02:00
🔀 Merge remote-tracking branch 'origin/develop'
This commit is contained in:
commit
4fcf0e282e
50 changed files with 1780 additions and 460 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,5 +5,6 @@ venv
|
|||
.env
|
||||
public
|
||||
*.swp
|
||||
argos-config.yaml
|
||||
config.yaml
|
||||
dist
|
||||
|
|
|
@ -58,6 +58,7 @@ pages:
|
|||
- pwd
|
||||
- ls
|
||||
- make docs
|
||||
- echo "https://framasoft.frama.io/framaspace/argos/* https://argos-monitoring.framasoft.org/:splat 301" > public/_redirects
|
||||
artifacts:
|
||||
paths:
|
||||
- public/
|
||||
|
|
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -2,6 +2,17 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
- 💄📯 — 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)
|
||||
- 📝 — Improve documentation
|
||||
- ✨ — Add command to warn if it’s been long since last viewing an agent (fix #49)
|
||||
- 💥 — Change default config file path to argos-config.yaml (fix #36)
|
||||
- 📝 — New documentation URL: doc is now on https://argos-monitoring.framasoft.org/
|
||||
- 💥 — Remove env vars and only use the configuration file
|
||||
- ✨ — Add built-in authentication for human interface
|
||||
|
||||
## 0.1.1
|
||||
|
||||
Date: 2024-04-30
|
||||
|
|
|
@ -2,13 +2,15 @@
|
|||
|
||||
A monitoring and status board for your websites.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
Internally, a HTTP API is exposed, and a job queue is used to distribute the checks to the agents.
|
||||
|
||||
- [Online documentation](http://framasoft.frama.io/framaspace/argos)
|
||||
- [Online documentation](https://argos-monitoring.framasoft.org/)
|
||||
- [Issue tracker](https://framagit.org/framasoft/framaspace/argos/-/issues)
|
||||
|
||||
## Requirements
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import asyncio
|
||||
import os
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
from sys import exit as sysexit
|
||||
from uuid import uuid4
|
||||
|
||||
import click
|
||||
|
@ -32,13 +34,24 @@ def coroutine(f):
|
|||
|
||||
|
||||
def validate_config_access(ctx, param, value):
|
||||
if os.path.isfile(value) and os.access(value, os.R_OK):
|
||||
return value
|
||||
for file in list(
|
||||
dict.fromkeys([value, "argos-config.yaml", "/etc/argos/config.yaml"])
|
||||
):
|
||||
path = Path(file)
|
||||
|
||||
if os.path.isfile(value):
|
||||
raise click.BadParameter(f"the file {value} is not readabale.")
|
||||
if path.is_file() and os.access(path, os.R_OK):
|
||||
return file
|
||||
|
||||
raise click.BadParameter(f"the file {value} does not exists or is not reachable.")
|
||||
if value == "argos-config.yaml":
|
||||
raise click.BadParameter(
|
||||
f"the file {value} does not exists or is not reachable, "
|
||||
"nor does /etc/argos/config.yaml."
|
||||
)
|
||||
|
||||
raise click.BadParameter(
|
||||
f"the file {value} does not exists or is not reachable, "
|
||||
"nor does argos-config.yaml or /etc/argos/config.yaml."
|
||||
)
|
||||
|
||||
|
||||
@click.group()
|
||||
|
@ -48,11 +61,17 @@ def cli():
|
|||
|
||||
@cli.group()
|
||||
def server():
|
||||
pass
|
||||
"""Commands for managing server, server’s configuration and users"""
|
||||
|
||||
|
||||
@server.group()
|
||||
def user():
|
||||
"""User management"""
|
||||
|
||||
|
||||
@cli.command()
|
||||
def version():
|
||||
"""Prints Argos’ version and exits"""
|
||||
click.echo(VERSION)
|
||||
|
||||
|
||||
|
@ -75,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"
|
||||
|
||||
|
@ -99,9 +118,10 @@ def agent(server_url, auth, max_tasks, wait_time, log_level):
|
|||
@click.option("--port", default=8000, type=int, help="Port to bind")
|
||||
@click.option(
|
||||
"--config",
|
||||
default="config.yaml",
|
||||
default="argos-config.yaml",
|
||||
help="Path of the configuration file. "
|
||||
"If ARGOS_YAML_FILE environment variable is set, its value will be used instead.",
|
||||
"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,
|
||||
)
|
||||
|
@ -109,7 +129,7 @@ def agent(server_url, auth, max_tasks, wait_time, log_level):
|
|||
def start(host, port, config, reload):
|
||||
"""Starts the server (use only for testing or development!)
|
||||
|
||||
See https://framasoft.frama.io/framaspace/argos/deployment/systemd.html#server
|
||||
See https://argos-monitoring.framasoft.org/deployment/systemd.html#server
|
||||
for advices on how to start the server for production.
|
||||
"""
|
||||
os.environ["ARGOS_YAML_FILE"] = config
|
||||
|
@ -147,9 +167,10 @@ def validate_max_results(ctx, param, value):
|
|||
)
|
||||
@click.option(
|
||||
"--config",
|
||||
default="config.yaml",
|
||||
default="argos-config.yaml",
|
||||
help="Path of the configuration file. "
|
||||
"If ARGOS_YAML_FILE environment variable is set, its value will be used instead.",
|
||||
"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,
|
||||
)
|
||||
|
@ -175,12 +196,48 @@ async def cleandb(max_results, max_lock_seconds, config):
|
|||
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,
|
||||
)
|
||||
@click.option(
|
||||
"--config",
|
||||
default="argos-config.yaml",
|
||||
help="Path of the configuration file. "
|
||||
"If ARGOS_YAML_FILE environment variable is set, its value will be used instead.",
|
||||
envvar="ARGOS_YAML_FILE",
|
||||
callback=validate_config_access,
|
||||
)
|
||||
@coroutine
|
||||
async def watch_agents(time_without_agent, config):
|
||||
"""Watch agents (to run routinely)
|
||||
|
||||
Issues a warning if no agent has been seen by the server for a given time.
|
||||
"""
|
||||
# It’s mandatory to do it before the imports
|
||||
os.environ["ARGOS_YAML_FILE"] = config
|
||||
|
||||
# The imports are made here otherwise the agent will need server configuration files.
|
||||
from argos.server import queries
|
||||
|
||||
db = await get_db()
|
||||
agents = await queries.get_recent_agents_count(db, time_without_agent)
|
||||
if agents == 0:
|
||||
click.echo(f"No agent has been seen in the last {time_without_agent} minutes.")
|
||||
sysexit(1)
|
||||
|
||||
|
||||
@server.command(short_help="Load or reload tasks’ configuration")
|
||||
@click.option(
|
||||
"--config",
|
||||
default="config.yaml",
|
||||
default="argos-config.yaml",
|
||||
help="Path of the configuration file. "
|
||||
"If ARGOS_YAML_FILE environment variable is set, its value will be used instead.",
|
||||
"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,
|
||||
)
|
||||
|
@ -192,15 +249,12 @@ async def reload_config(config):
|
|||
|
||||
# The imports are made here otherwise the agent will need server configuration files.
|
||||
from argos.server import queries
|
||||
from argos.server.main import get_application, read_config
|
||||
from argos.server.settings import get_app_settings
|
||||
from argos.server.main import read_config
|
||||
|
||||
appli = get_application()
|
||||
settings = get_app_settings()
|
||||
config = read_config(appli, settings)
|
||||
_config = read_config(config)
|
||||
|
||||
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['vanished']} tasks deleted")
|
||||
|
@ -209,9 +263,10 @@ async def reload_config(config):
|
|||
@server.command()
|
||||
@click.option(
|
||||
"--config",
|
||||
default="config.yaml",
|
||||
default="argos-config.yaml",
|
||||
help="Path of the configuration file. "
|
||||
"If ARGOS_YAML_FILE environment variable is set, its value will be used instead.",
|
||||
"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,
|
||||
)
|
||||
|
@ -222,16 +277,253 @@ async def migrate(config):
|
|||
os.environ["ARGOS_YAML_FILE"] = config
|
||||
|
||||
# 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 = os.path.dirname(__file__)
|
||||
alembic_cfg = Config(os.path.join(current_dir, "server/migrations/alembic.ini"))
|
||||
alembic_cfg.set_main_option("sqlalchemy.url", settings.database_url)
|
||||
current_dir = Path(__file__).resolve().parent
|
||||
alembic_cfg = Config(current_dir / "server" / "migrations" / "alembic.ini")
|
||||
alembic_cfg.set_main_option("sqlalchemy.url", str(settings.general.db.url))
|
||||
command.upgrade(alembic_cfg, "head")
|
||||
|
||||
|
||||
@user.command()
|
||||
@click.option(
|
||||
"--config",
|
||||
default="argos-config.yaml",
|
||||
help="Path of the configuration file. "
|
||||
"If ARGOS_YAML_FILE environment variable is set, its value will be used instead.",
|
||||
envvar="ARGOS_YAML_FILE",
|
||||
callback=validate_config_access,
|
||||
)
|
||||
@click.option("--name", prompt=True, help="Name of the user to create.")
|
||||
@click.password_option()
|
||||
@coroutine
|
||||
async def add(config, name, password):
|
||||
"""Add new user"""
|
||||
# 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
|
||||
from passlib.context import CryptContext
|
||||
|
||||
db = await get_db()
|
||||
_user = await queries.get_user(db, name)
|
||||
if _user is not None:
|
||||
click.echo(f"User {name} already exists.")
|
||||
sysexit(1)
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
await queries.add_user(db, name, pwd_context.hash(password))
|
||||
click.echo(f"User {name} added.")
|
||||
|
||||
|
||||
@user.command()
|
||||
@click.option(
|
||||
"--config",
|
||||
default="argos-config.yaml",
|
||||
help="Path of the configuration file. "
|
||||
"If ARGOS_YAML_FILE environment variable is set, its value will be used instead.",
|
||||
envvar="ARGOS_YAML_FILE",
|
||||
callback=validate_config_access,
|
||||
)
|
||||
@click.option(
|
||||
"--name", prompt=True, help="Name of the user you want to change the password."
|
||||
)
|
||||
@click.password_option()
|
||||
@coroutine
|
||||
async def change_password(config, name, password):
|
||||
"""Change user’s password"""
|
||||
# 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
|
||||
from passlib.context import CryptContext
|
||||
|
||||
db = await get_db()
|
||||
_user = await queries.get_user(db, name)
|
||||
if _user is None:
|
||||
click.echo(f"User {name} does not exist.")
|
||||
sysexit(1)
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
_user.password = pwd_context.hash(password)
|
||||
db.commit()
|
||||
click.echo(f"Password of user {name} changed.")
|
||||
|
||||
|
||||
@user.command()
|
||||
@click.option(
|
||||
"--config",
|
||||
default="argos-config.yaml",
|
||||
help="Path of the configuration file. "
|
||||
"If ARGOS_YAML_FILE environment variable is set, its value will be used instead.",
|
||||
envvar="ARGOS_YAML_FILE",
|
||||
callback=validate_config_access,
|
||||
)
|
||||
@click.option(
|
||||
"--name", required=True, help="Name of the user you want to test the password for."
|
||||
)
|
||||
@click.option("--password", prompt=True, hide_input=True)
|
||||
@coroutine
|
||||
async def verify_password(config, name, password):
|
||||
"""Test user’s password"""
|
||||
# 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
|
||||
from passlib.context import CryptContext
|
||||
|
||||
db = await get_db()
|
||||
_user = await queries.get_user(db, name)
|
||||
if _user is None:
|
||||
click.echo(f"User {name} does not exist.")
|
||||
sysexit(1)
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
if not pwd_context.verify(password, _user.password):
|
||||
click.echo("Wrong password!")
|
||||
sysexit(2)
|
||||
|
||||
click.echo("The provided password is correct.")
|
||||
|
||||
|
||||
@user.command()
|
||||
@click.option(
|
||||
"--config",
|
||||
default="argos-config.yaml",
|
||||
help="Path of the configuration file. "
|
||||
"If ARGOS_YAML_FILE environment variable is set, its value will be used instead.",
|
||||
envvar="ARGOS_YAML_FILE",
|
||||
callback=validate_config_access,
|
||||
)
|
||||
@click.option("--name", required=True, help="Name of the user to disable.")
|
||||
@coroutine
|
||||
async def disable(config, name):
|
||||
"""Disable user"""
|
||||
# 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()
|
||||
_user = await queries.get_user(db, name)
|
||||
if _user is None:
|
||||
click.echo(f"User {name} does not exist.")
|
||||
sysexit(1)
|
||||
if _user.disabled:
|
||||
click.echo(f"User {name} is already disabled.")
|
||||
sysexit(2)
|
||||
|
||||
_user.disabled = True
|
||||
db.commit()
|
||||
|
||||
click.echo(f"User {name} disabled.")
|
||||
|
||||
|
||||
@user.command()
|
||||
@click.option(
|
||||
"--config",
|
||||
default="argos-config.yaml",
|
||||
help="Path of the configuration file. "
|
||||
"If ARGOS_YAML_FILE environment variable is set, its value will be used instead.",
|
||||
envvar="ARGOS_YAML_FILE",
|
||||
callback=validate_config_access,
|
||||
)
|
||||
@click.option("--name", required=True, help="Name of the user to reenable")
|
||||
@coroutine
|
||||
async def enable(config, name):
|
||||
"""Enable user"""
|
||||
# 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()
|
||||
_user = await queries.get_user(db, name)
|
||||
if _user is None:
|
||||
click.echo(f"User {name} does not exist.")
|
||||
sysexit(1)
|
||||
if not _user.disabled:
|
||||
click.echo(f"User {name} is already enabled.")
|
||||
sysexit(2)
|
||||
|
||||
_user.disabled = False
|
||||
db.commit()
|
||||
|
||||
click.echo(f"User {name} enabled.")
|
||||
|
||||
|
||||
@user.command()
|
||||
@click.option(
|
||||
"--config",
|
||||
default="argos-config.yaml",
|
||||
help="Path of the configuration file. "
|
||||
"If ARGOS_YAML_FILE environment variable is set, its value will be used instead.",
|
||||
envvar="ARGOS_YAML_FILE",
|
||||
callback=validate_config_access,
|
||||
)
|
||||
@click.option("--name", required=True, help="Name of the user to delete.")
|
||||
@coroutine
|
||||
async def delete(config, name):
|
||||
"""Delete user"""
|
||||
# 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()
|
||||
_user = await queries.get_user(db, name)
|
||||
if _user is None:
|
||||
click.echo(f"User {name} does not exist.")
|
||||
sysexit(1)
|
||||
|
||||
db.delete(_user)
|
||||
db.commit()
|
||||
|
||||
click.echo(f"User {name} deleted.")
|
||||
|
||||
|
||||
@user.command()
|
||||
@click.option(
|
||||
"--config",
|
||||
default="argos-config.yaml",
|
||||
help="Path of the configuration file. "
|
||||
"If ARGOS_YAML_FILE environment variable is set, its value will be used instead.",
|
||||
envvar="ARGOS_YAML_FILE",
|
||||
callback=validate_config_access,
|
||||
)
|
||||
@coroutine
|
||||
async def show(config):
|
||||
"""List all users"""
|
||||
# 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()
|
||||
users = await queries.list_users(db)
|
||||
if users.count() == 0:
|
||||
click.echo("There is no users in database.")
|
||||
sysexit(1)
|
||||
|
||||
click.echo("✅ means that the user is enabled.")
|
||||
click.echo("❌ means that the user is disabled.")
|
||||
|
||||
for _user in users.all():
|
||||
status = "✅"
|
||||
if _user.disabled:
|
||||
status = "❌"
|
||||
click.echo(f"{status} {_user.username}, last login: {_user.last_login_at}")
|
||||
|
||||
|
||||
@server.command(short_help="Generate a token for agents")
|
||||
@coroutine
|
||||
async def generate_token():
|
||||
|
@ -242,5 +534,19 @@ async def generate_token():
|
|||
click.echo(uuid4())
|
||||
|
||||
|
||||
@server.command()
|
||||
@coroutine
|
||||
async def generate_config():
|
||||
"""Output a self-documented example config file.
|
||||
|
||||
\b
|
||||
Redirect the output to a file to save it:
|
||||
argos server generate-config > /etc/argos/config.yaml
|
||||
"""
|
||||
config_example = Path(__file__).resolve().parent / "config-example.yaml"
|
||||
with config_example.open("r", encoding="utf-8") as f:
|
||||
print(f.read())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
|
|
88
argos/config-example.yaml
Normal file
88
argos/config-example.yaml
Normal file
|
@ -0,0 +1,88 @@
|
|||
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"
|
||||
# to get a good string for cookie_secret, run:
|
||||
# openssl rand -hex 32
|
||||
cookie_secret: "foo_bar_baz"
|
||||
# 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
|
||||
warning:
|
||||
- local
|
||||
critical:
|
||||
- local
|
||||
unknown:
|
||||
- local
|
||||
# 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:
|
||||
# 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: '<div id= "mypads"></div>'
|
||||
- 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
|
|
@ -1,7 +1,14 @@
|
|||
import logging
|
||||
|
||||
|
||||
logging.getLogger("passlib").setLevel(logging.ERROR)
|
||||
|
||||
|
||||
LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
||||
|
||||
# Print level before message
|
||||
logging.basicConfig(format="%(levelname)-9s %(message)s")
|
||||
|
||||
# XXX We probably want different loggers for client and server.
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -8,17 +8,27 @@ from pydantic import (
|
|||
BaseModel,
|
||||
ConfigDict,
|
||||
HttpUrl,
|
||||
PostgresDsn,
|
||||
StrictBool,
|
||||
EmailStr,
|
||||
PositiveInt,
|
||||
field_validator,
|
||||
)
|
||||
from pydantic.functional_validators import BeforeValidator
|
||||
from pydantic.networks import UrlConstraints
|
||||
from pydantic_core import Url
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from argos.schemas.utils import string_to_duration
|
||||
|
||||
Severity = Literal["warning", "error", "critical", "unknown"]
|
||||
Environment = Literal["dev", "test", "production"]
|
||||
SQLiteDsn = Annotated[
|
||||
Url,
|
||||
UrlConstraints(
|
||||
allowed_schemes=["sqlite"],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def parse_threshold(value):
|
||||
|
@ -134,10 +144,19 @@ class GotifyUrl(BaseModel):
|
|||
tokens: List[str]
|
||||
|
||||
|
||||
class DbSettings(BaseModel):
|
||||
url: PostgresDsn | SQLiteDsn
|
||||
pool_size: int = 10
|
||||
max_overflow: int = 20
|
||||
|
||||
|
||||
class General(BaseModel):
|
||||
"""Frequency for the checks and alerts"""
|
||||
|
||||
cookie_secret: str
|
||||
frequency: int
|
||||
db: DbSettings
|
||||
env: Environment = "production"
|
||||
alerts: Alert
|
||||
mail: Optional[Mail] = None
|
||||
gotify: Optional[List[GotifyUrl]] = None
|
||||
|
|
|
@ -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}
|
||||
|
|
13
argos/server/exceptions.py
Normal file
13
argos/server/exceptions.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from fastapi import Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
|
||||
class NotAuthenticatedException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def auth_exception_handler(request: Request, exc: NotAuthenticatedException):
|
||||
"""
|
||||
Redirect the user to the login page if not logged in
|
||||
"""
|
||||
return RedirectResponse(url=request.url_for("login_view"))
|
|
@ -1,79 +1,59 @@
|
|||
import os
|
||||
import sys
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi_login import LoginManager
|
||||
from pydantic import ValidationError
|
||||
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.settings import get_app_settings, read_yaml_config
|
||||
from argos.server import models, routes, queries
|
||||
from argos.server.exceptions import NotAuthenticatedException, auth_exception_handler
|
||||
from argos.server.settings import read_yaml_config
|
||||
|
||||
|
||||
def get_application() -> FastAPI:
|
||||
"""Spawn Argos FastAPI server"""
|
||||
settings = get_app_settings()
|
||||
appli = FastAPI()
|
||||
appli = FastAPI(lifespan=lifespan)
|
||||
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)
|
||||
appli.state.config = config
|
||||
appli.state.settings = settings
|
||||
appli.add_exception_handler(NotAuthenticatedException, auth_exception_handler)
|
||||
appli.state.manager = create_manager(config.general.cookie_secret)
|
||||
|
||||
@appli.state.manager.user_loader()
|
||||
async def query_user(user: str) -> None | models.User:
|
||||
"""
|
||||
Get a user from the db
|
||||
:param user: name of the user
|
||||
:return: None or the user object
|
||||
"""
|
||||
return await queries.get_user(appli.state.db, user)
|
||||
|
||||
appli.add_event_handler(
|
||||
"startup",
|
||||
create_start_app_handler(appli),
|
||||
)
|
||||
appli.add_event_handler(
|
||||
"shutdown",
|
||||
create_stop_app_handler(appli),
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
def create_start_app_handler(appli):
|
||||
"""Warmup the server:
|
||||
setup database connection
|
||||
"""
|
||||
|
||||
async def _get_db():
|
||||
setup_database(appli)
|
||||
|
||||
return await connect_to_db(appli)
|
||||
|
||||
return _get_db
|
||||
|
||||
|
||||
async def connect_to_db(appli):
|
||||
appli.state.db = appli.state.SessionLocal()
|
||||
return appli.state.db
|
||||
|
||||
|
||||
def create_stop_app_handler(appli):
|
||||
"""Gracefully shutdown the server:
|
||||
close database connection.
|
||||
"""
|
||||
|
||||
async def stop_app():
|
||||
appli.state.db.close()
|
||||
|
||||
return stop_app
|
||||
|
||||
|
||||
def read_config(appli, settings):
|
||||
def read_config(yaml_file):
|
||||
try:
|
||||
config = read_yaml_config(settings.yaml_file)
|
||||
appli.state.config = config
|
||||
config = read_yaml_config(yaml_file)
|
||||
return config
|
||||
except ValidationError as err:
|
||||
logger.error("Errors where found while reading configuration:")
|
||||
|
@ -83,25 +63,26 @@ def read_config(appli, settings):
|
|||
|
||||
|
||||
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}
|
||||
logger.debug("Using database URL %s", settings.database_url)
|
||||
if settings.database_url.startswith("sqlite:////tmp"):
|
||||
if config.general.env == "production" and db_url.startswith("sqlite:////tmp"):
|
||||
logger.warning("Using sqlite in /tmp is not recommended for production")
|
||||
|
||||
extra_settings = {}
|
||||
if settings.db_pool_size:
|
||||
extra_settings.setdefault("pool_size", settings.db_pool_size)
|
||||
if config.general.db.pool_size:
|
||||
extra_settings.setdefault("pool_size", config.general.db.pool_size)
|
||||
|
||||
if settings.db_max_overflow:
|
||||
extra_settings.setdefault("max_overflow", settings.db_max_overflow)
|
||||
if config.general.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):
|
||||
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)
|
||||
|
||||
appli.state.SessionLocal = sessionmaker(
|
||||
|
@ -111,4 +92,40 @@ def setup_database(appli):
|
|||
models.Base.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
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",
|
||||
use_cookie=True,
|
||||
use_header=False,
|
||||
not_authenticated_exception=NotAuthenticatedException,
|
||||
)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(appli):
|
||||
"""Server start and stop actions
|
||||
|
||||
Setup database connection then close it at shutdown.
|
||||
"""
|
||||
setup_database(appli)
|
||||
|
||||
db = await connect_to_db(appli)
|
||||
|
||||
tasks_count = await queries.count_tasks(db)
|
||||
if tasks_count == 0:
|
||||
logger.warning(
|
||||
"There is no tasks in the database. "
|
||||
'Please launch the command "argos server reload-config"'
|
||||
)
|
||||
|
||||
yield
|
||||
|
||||
appli.state.db.close()
|
||||
|
||||
|
||||
app = get_application()
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
"""Add users table
|
||||
|
||||
Revision ID: c780864dc407
|
||||
Revises: defda3f2952d
|
||||
Create Date: 2024-06-10 16:31:17.296983
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "c780864dc407"
|
||||
down_revision: Union[str, None] = "defda3f2952d"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"users",
|
||||
sa.Column("username", sa.String(), nullable=False),
|
||||
sa.Column("password", sa.String(), nullable=False),
|
||||
sa.Column("disabled", sa.Boolean(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("last_login_at", sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("username"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("users")
|
|
@ -138,3 +138,18 @@ class ConfigCache(Base):
|
|||
name: Mapped[str] = mapped_column(primary_key=True)
|
||||
val: Mapped[str] = mapped_column()
|
||||
updated_at: Mapped[datetime] = mapped_column()
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""Database model for user authentication"""
|
||||
|
||||
__tablename__ = "users"
|
||||
username: Mapped[str] = mapped_column(primary_key=True)
|
||||
password: Mapped[str] = mapped_column()
|
||||
disabled: Mapped[bool] = mapped_column()
|
||||
created_at: Mapped[datetime] = mapped_column(default=datetime.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(nullable=True)
|
||||
last_login_at: Mapped[datetime] = mapped_column(nullable=True)
|
||||
|
||||
def update_last_login_at(self):
|
||||
self.last_login_at = datetime.now()
|
||||
|
|
|
@ -4,12 +4,12 @@ from hashlib import sha256
|
|||
from typing import List
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from sqlalchemy import desc, func
|
||||
from sqlalchemy import asc, desc, func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from argos import schemas
|
||||
from argos.logging import logger
|
||||
from argos.server.models import Result, Task, ConfigCache
|
||||
from argos.server.models import Result, Task, ConfigCache, User
|
||||
|
||||
|
||||
async def list_tasks(db: Session, agent_id: str, limit: int = 100):
|
||||
|
@ -32,6 +32,25 @@ async def list_tasks(db: Session, agent_id: str, limit: int = 100):
|
|||
return tasks
|
||||
|
||||
|
||||
async def add_user(db: Session, name: str, password: str) -> User:
|
||||
user = User(
|
||||
username=name,
|
||||
password=password,
|
||||
disabled=False,
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
return user
|
||||
|
||||
|
||||
async def get_user(db: Session, username: str) -> None | User:
|
||||
return db.get(User, username)
|
||||
|
||||
|
||||
async def list_users(db: Session):
|
||||
return db.query(User).order_by(asc(User.username))
|
||||
|
||||
|
||||
async def get_task(db: Session, task_id: int) -> Task:
|
||||
return db.get(Task, task_id)
|
||||
|
||||
|
@ -48,12 +67,13 @@ 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: None | bool = None):
|
||||
query = db.query(Task)
|
||||
if selected:
|
||||
query = query.filter(Task.selected_by is not None)
|
||||
else:
|
||||
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()
|
||||
|
||||
|
@ -240,3 +260,11 @@ async def release_old_locks(db: Session, max_lock_seconds: int):
|
|||
)
|
||||
db.commit()
|
||||
return updated
|
||||
|
||||
|
||||
async def get_recent_agents_count(db: Session, minutes: int):
|
||||
"""Get agents seen less than <minutes> ago"""
|
||||
max_time = datetime.now() - timedelta(minutes=minutes)
|
||||
|
||||
agents = db.query(Result.agent_id).filter(Result.submitted_at > max_time).distinct()
|
||||
return agents.count()
|
||||
|
|
|
@ -16,6 +16,10 @@ def get_config(request: Request):
|
|||
return request.app.state.config
|
||||
|
||||
|
||||
async def get_manager(request: Request):
|
||||
return await request.app.state.manager(request)
|
||||
|
||||
|
||||
async def verify_token(
|
||||
request: Request, token: HTTPAuthorizationCredentials = Depends(auth_scheme)
|
||||
):
|
||||
|
|
|
@ -1,31 +1,99 @@
|
|||
"""Web interface for humans"""
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from functools import cmp_to_key
|
||||
from os import path
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from fastapi import APIRouter, Cookie, Depends, Form, Request, status
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from passlib.context import CryptContext
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from argos.schemas import Config
|
||||
from argos.server import queries
|
||||
from argos.server.models import Result, Task
|
||||
from argos.server.routes.dependencies import get_config, get_db
|
||||
from argos.server.models import Result, Task, User
|
||||
from argos.server.routes.dependencies import get_config, get_db, get_manager
|
||||
|
||||
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}
|
||||
|
||||
|
||||
@route.get("/login")
|
||||
async def login_view(request: Request, msg: str | None = None):
|
||||
token = request.cookies.get("access-token")
|
||||
if token is not None:
|
||||
manager = get_manager(request)
|
||||
user = await manager.get_current_user(token)
|
||||
if user is not None:
|
||||
return RedirectResponse(
|
||||
request.url_for("get_severity_counts_view"),
|
||||
status_code=status.HTTP_303_SEE_OTHER,
|
||||
)
|
||||
|
||||
if msg == "logout":
|
||||
msg = "You have been successfully disconnected."
|
||||
else:
|
||||
msg = None
|
||||
|
||||
return templates.TemplateResponse("login.html", {"request": request, "msg": msg})
|
||||
|
||||
|
||||
@route.post("/login")
|
||||
async def post_login(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
data: OAuth2PasswordRequestForm = Depends(),
|
||||
):
|
||||
username = data.username
|
||||
user = await queries.get_user(db, username)
|
||||
invalid_credentials = templates.TemplateResponse(
|
||||
"login.html",
|
||||
{"request": request, "msg": "Sorry, invalid username or bad password."},
|
||||
)
|
||||
if user is None:
|
||||
return invalid_credentials
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
if not pwd_context.verify(data.password, user.password):
|
||||
return invalid_credentials
|
||||
|
||||
user.last_login_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
manager = request.app.state.manager
|
||||
token = manager.create_access_token(
|
||||
data={"sub": username}, expires=timedelta(days=7)
|
||||
)
|
||||
response = RedirectResponse(
|
||||
request.url_for("get_severity_counts_view"),
|
||||
status_code=status.HTTP_303_SEE_OTHER,
|
||||
)
|
||||
manager.set_cookie(response, token)
|
||||
return response
|
||||
|
||||
|
||||
@route.get("/logout")
|
||||
async def logout_view(request: Request, user: User | None = Depends(get_manager)):
|
||||
response = RedirectResponse(
|
||||
request.url_for("login_view").include_query_params(msg="logout"),
|
||||
status_code=status.HTTP_303_SEE_OTHER,
|
||||
)
|
||||
response.delete_cookie(key="access-token")
|
||||
return response
|
||||
|
||||
|
||||
@route.get("/")
|
||||
async def get_severity_counts_view(
|
||||
request: Request,
|
||||
user: User | None = Depends(get_manager),
|
||||
db: Session = Depends(get_db),
|
||||
auto_refresh_enabled: Annotated[bool, Cookie()] = False,
|
||||
auto_refresh_seconds: Annotated[int, Cookie()] = 15,
|
||||
|
@ -48,7 +116,11 @@ async def get_severity_counts_view(
|
|||
|
||||
|
||||
@route.get("/domains")
|
||||
async def get_domains_view(request: Request, db: Session = Depends(get_db)):
|
||||
async def get_domains_view(
|
||||
request: Request,
|
||||
user: User | None = Depends(get_manager),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Show all tasks and their current state"""
|
||||
tasks = db.query(Task).all()
|
||||
|
||||
|
@ -96,7 +168,10 @@ async def get_domains_view(request: Request, db: Session = Depends(get_db)):
|
|||
|
||||
@route.get("/domain/{domain}")
|
||||
async def get_domain_tasks_view(
|
||||
request: Request, domain: str, db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
domain: str,
|
||||
user: User | None = Depends(get_manager),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Show all tasks attached to a domain"""
|
||||
tasks = db.query(Task).filter(Task.domain.contains(f"//{domain}")).all()
|
||||
|
@ -107,7 +182,10 @@ async def get_domain_tasks_view(
|
|||
|
||||
@route.get("/result/{result_id}")
|
||||
async def get_result_view(
|
||||
request: Request, result_id: int, db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
result_id: int,
|
||||
user: User | None = Depends(get_manager),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Show the details of a result"""
|
||||
result = db.query(Result).get(result_id)
|
||||
|
@ -120,6 +198,7 @@ async def get_result_view(
|
|||
async def get_task_results_view(
|
||||
request: Request,
|
||||
task_id: int,
|
||||
user: User | None = Depends(get_manager),
|
||||
db: Session = Depends(get_db),
|
||||
config: Config = Depends(get_config),
|
||||
):
|
||||
|
@ -144,7 +223,11 @@ async def get_task_results_view(
|
|||
|
||||
|
||||
@route.get("/agents")
|
||||
async def get_agents_view(request: Request, db: Session = Depends(get_db)):
|
||||
async def get_agents_view(
|
||||
request: Request,
|
||||
user: User | None = Depends(get_manager),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Show argos agents and the last time the server saw them"""
|
||||
last_seen = (
|
||||
db.query(Result.agent_id, func.max(Result.submitted_at).label("submitted_at"))
|
||||
|
@ -160,6 +243,7 @@ async def get_agents_view(request: Request, db: Session = Depends(get_db)):
|
|||
@route.post("/refresh")
|
||||
async def set_refresh_cookies_view(
|
||||
request: Request,
|
||||
user: User | None = Depends(get_manager),
|
||||
auto_refresh_enabled: Annotated[bool, Form()] = False,
|
||||
auto_refresh_seconds: Annotated[int, Form()] = 15,
|
||||
):
|
||||
|
@ -168,5 +252,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
|
||||
|
|
|
@ -1,85 +1,21 @@
|
|||
"""Pydantic schemas for server"""
|
||||
import os
|
||||
from functools import lru_cache
|
||||
from os import environ
|
||||
from typing import Optional, Union
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from yamlinclude import YamlIncludeConstructor
|
||||
|
||||
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 config.yaml as config file.
|
||||
Uses a SQLite database."""
|
||||
|
||||
app_env: str = "dev"
|
||||
yaml_file: str = "config.yaml"
|
||||
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):
|
||||
parsed = _load_yaml(filename)
|
||||
return Config(**parsed)
|
||||
|
||||
|
||||
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:
|
||||
|
|
7
argos/server/static/pico.min.css
vendored
7
argos/server/static/pico.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -13,7 +13,7 @@ body > main {
|
|||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: calc(var(--typography-spacing-vertical) * 0.5);
|
||||
margin-bottom: calc(var(--pico-typography-spacing-vertical) * 0.5);
|
||||
}
|
||||
|
||||
.grid-index {
|
||||
|
@ -26,10 +26,12 @@ h2 {
|
|||
.grid-index article {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: calc(var(--block-spacing-vertical) * 0.7);
|
||||
padding-bottom: calc(var(--pico-block-spacing-vertical) * 2.4);
|
||||
}
|
||||
.grid-index article > header {
|
||||
margin-bottom: calc(var(--block-spacing-vertical) * 0.7);
|
||||
padding-top: calc(var(--pico-block-spacing-vertical) * 2.4);
|
||||
padding-bottom: calc(var(--pico-block-spacing-vertical) * 2.4);
|
||||
margin-bottom: calc(var(--pico-block-spacing-vertical) * 2.4);
|
||||
}
|
||||
|
||||
label[for="select-status"] {
|
||||
|
|
|
@ -32,31 +32,52 @@
|
|||
</li>
|
||||
</ul>
|
||||
</a>
|
||||
{% if request.url.remove_query_params('msg') != url_for('login_view') %}
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{{ url_for('get_severity_counts_view') }}"
|
||||
class="outline {{ 'contrast' if request.url == url_for('get_severity_counts_view') }}"
|
||||
role="button">
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="{{ url_for('get_domains_view') }}"
|
||||
class="outline {{ 'contrast' if request.url == url_for('get_domains_view') }}"
|
||||
role="button">
|
||||
Domains
|
||||
</a>
|
||||
<a href="{{ url_for('get_agents_view') }}"
|
||||
class="outline {{ 'contrast' if request.url == url_for('get_agents_view') }}"
|
||||
role="button">
|
||||
Agents
|
||||
</a>
|
||||
<a href="#"
|
||||
id="reschedule-all"
|
||||
class="outline"
|
||||
title="Reschedule non-ok checks as soon as possible">
|
||||
🕐
|
||||
</a>
|
||||
</li>
|
||||
<details class="dropdown">
|
||||
<summary autofocus>Menu</summary>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{{ url_for('get_severity_counts_view') }}"
|
||||
class="outline {{ 'contrast' if request.url == url_for('get_severity_counts_view') }}"
|
||||
role="button">
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('get_domains_view') }}"
|
||||
class="outline {{ 'contrast' if request.url == url_for('get_domains_view') }}"
|
||||
role="button">
|
||||
Domains
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('get_agents_view') }}"
|
||||
class="outline {{ 'contrast' if request.url == url_for('get_agents_view') }}"
|
||||
role="button">
|
||||
Agents
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#"
|
||||
id="reschedule-all"
|
||||
class="outline"
|
||||
title="Reschedule non-ok checks as soon as possible"
|
||||
role="button">
|
||||
Reschedule non-ok checks
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('logout_view') }}"
|
||||
class="outline {{ 'contrast' if request.url == url_for('get_agents_view') }}"
|
||||
role="button">
|
||||
Logout
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</header>
|
||||
<main class="container">
|
||||
|
@ -71,10 +92,10 @@
|
|||
</div>
|
||||
</main>
|
||||
<footer class="text-center">
|
||||
<a href="https://framasoft.frama.io/framaspace/argos/">Argos Panoptès</a>,
|
||||
<a href="https://argos-monitoring.framasoft.org/">Argos Panoptès</a>,
|
||||
<a href="https://framagit.org/framasoft/framaspace/argos/-/blob/main/LICENSE">AGPLv3</a>
|
||||
(<a href="https://framagit.org/framasoft/framaspace/argos">sources</a>)
|
||||
<br/>
|
||||
<br>
|
||||
API documentation:
|
||||
<a href="{{ url_for('get_severity_counts_view') }}docs">Swagger</a>
|
||||
or
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
name="auto_refresh_seconds"
|
||||
type="number"
|
||||
form="refresh-form"
|
||||
min="1"
|
||||
min="5"
|
||||
value="{{ auto_refresh_seconds }}"> seconds
|
||||
</label>
|
||||
</li>
|
||||
|
|
25
argos/server/templates/login.html
Normal file
25
argos/server/templates/login.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}<h2>Login</h2>{% endblock title %}
|
||||
{% block content %}
|
||||
{% if msg is not none %}
|
||||
<article>{{ msg }}</article>
|
||||
{% endif %}
|
||||
<div class="frame">
|
||||
<label for="username">Username</label>
|
||||
<input id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
form="login"
|
||||
autofocus>
|
||||
<label for="password">Password</label>
|
||||
<input id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
form="login">
|
||||
<form id="login"
|
||||
method="post"
|
||||
action="{{ url_for('post_login') }}">
|
||||
<input type="Submit">
|
||||
</form>
|
||||
</div>
|
||||
{% endblock content %}
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}<h2>{{ result }}</h2>{% endblock title %}
|
||||
{% block title %}<h2>Result {{ result.id }} - {{ result.status }}</h2>{% endblock title %}
|
||||
{% block content %}
|
||||
<dl>
|
||||
<dt>Task</dt>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
{% for result in results %}
|
||||
<tr>
|
||||
<tr id="{{ result.id }}">
|
||||
<td>{{ result.submitted_at }}</td>
|
||||
<td>{{ result.status }}</td>
|
||||
<td>{{ result.severity }}</td>
|
||||
|
@ -22,5 +22,4 @@
|
|||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endblock content %}
|
||||
|
|
|
@ -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
|
||||
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
|
||||
|
|
|
@ -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: '<div id= "mypads"></div>'
|
||||
- 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
|
1
conf/config-example.yaml
Symbolic link
1
conf/config-example.yaml
Symbolic link
|
@ -0,0 +1 @@
|
|||
../argos/config-example.yaml
|
5
conf/default-argos-agent
Normal file
5
conf/default-argos-agent
Normal file
|
@ -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
|
5
conf/default-argos-server
Normal file
5
conf/default-argos-server
Normal file
|
@ -0,0 +1,5 @@
|
|||
ARGOS_YAML_FILE="/etc/argos/config.yaml"
|
||||
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
|
|
@ -9,21 +9,15 @@ server {
|
|||
ssl_certificate /etc/letsencrypt/live/argos.example.org/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/argos.example.org/privkey.pem;
|
||||
|
||||
access_log /var/log/nginx/argos.example.org.access.log;
|
||||
error_log /var/log/nginx/argos.example.org.error.log;
|
||||
access_log /var/log/nginx/argos.example.org.access.log;
|
||||
error_log /var/log/nginx/argos.example.org.error.log;
|
||||
|
||||
if ($scheme != "https") {
|
||||
rewrite ^ https://$http_host$request_uri? permanent;
|
||||
}
|
||||
|
||||
location ~ ^/($|domains?/|result/|task/|refresh/) {
|
||||
auth_basic "Closed site";
|
||||
auth_basic_user_file argos.passwd;
|
||||
include proxy_params;
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
}
|
||||
location / {
|
||||
include proxy_params;
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
include proxy_params;
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
[Unit]
|
||||
Description=Argos agent
|
||||
Documentation=https://framasoft.frama.io/framaspace/argos/
|
||||
Documentation=https://argos-monitoring.framasoft.org/
|
||||
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
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
[Unit]
|
||||
Description=Argos server
|
||||
Documentation=https://framasoft.frama.io/framaspace/argos/
|
||||
Documentation=https://argos-monitoring.framasoft.org/
|
||||
Requires=network.target postgresql.service
|
||||
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
|
||||
|
|
|
@ -13,7 +13,7 @@ These checks are the most basic ones. They simply check that the response from t
|
|||
|
||||
```{code-block} yaml
|
||||
---
|
||||
caption: config.yaml
|
||||
caption: argos-config.yaml
|
||||
---
|
||||
- domain: "https://example.org"
|
||||
paths:
|
||||
|
@ -30,7 +30,7 @@ caption: config.yaml
|
|||
|
||||
```{code-block} yaml
|
||||
---
|
||||
caption: config.yaml
|
||||
caption: argos-config.yaml
|
||||
---
|
||||
ssl:
|
||||
thresholds:
|
||||
|
|
293
docs/cli.md
293
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
|
||||
```
|
||||
|
||||
<!--[[[end]]]
|
||||
|
@ -43,7 +43,7 @@ Commands:
|
|||
```man
|
||||
Usage: argos agent [OPTIONS] SERVER_URL AUTH
|
||||
|
||||
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"
|
||||
|
||||
|
@ -73,15 +73,20 @@ Options:
|
|||
```man
|
||||
Usage: argos server [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
Commands for managing server, server’s configuration and users
|
||||
|
||||
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 a self-documented 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!)
|
||||
user User management
|
||||
watch-agents Watch agents (to run routinely)
|
||||
```
|
||||
|
||||
<!--[[[end]]]
|
||||
|
@ -98,14 +103,16 @@ Usage: argos server start [OPTIONS]
|
|||
|
||||
Starts the server (use only for testing or development!)
|
||||
|
||||
See https://framasoft.frama.io/framaspace/argos/deployment/systemd.html#server
|
||||
for advices on how to start the server for production.
|
||||
See https://argos-monitoring.framasoft.org/deployment/systemd.html#server for
|
||||
advices on how to start the server for production.
|
||||
|
||||
Options:
|
||||
--host TEXT Host to bind
|
||||
--port INTEGER Port to bind
|
||||
--config TEXT Path of the configuration file. If ARGOS_YAML_FILE environment
|
||||
variable is set, its value will be used instead.
|
||||
variable is set, its value will be used instead. Default
|
||||
value: argos-config.yaml and /etc/argos/config.yaml as
|
||||
fallback.
|
||||
--reload Enable hot reloading
|
||||
--help Show this message and exit.
|
||||
```
|
||||
|
@ -127,7 +134,8 @@ Usage: argos server migrate [OPTIONS]
|
|||
|
||||
Options:
|
||||
--config TEXT Path of the configuration file. If ARGOS_YAML_FILE environment
|
||||
variable is set, its value will be used instead.
|
||||
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.
|
||||
```
|
||||
|
||||
|
@ -156,7 +164,38 @@ Options:
|
|||
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.
|
||||
used instead. Default value: argos-config.yaml and
|
||||
/etc/argos/config.yaml as fallback.
|
||||
--help Show this message and exit.
|
||||
```
|
||||
|
||||
<!--[[[end]]]
|
||||
-->
|
||||
|
||||
### Server watch-agents
|
||||
|
||||
<!--
|
||||
.. [[[cog
|
||||
help(["server", "cleandb", "--help"])
|
||||
.. ]]] -->
|
||||
|
||||
```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.
|
||||
```
|
||||
|
||||
|
@ -177,14 +216,37 @@ Usage: argos server reload-config [OPTIONS]
|
|||
|
||||
Options:
|
||||
--config TEXT Path of the configuration file. If ARGOS_YAML_FILE environment
|
||||
variable is set, its value will be used instead.
|
||||
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.
|
||||
```
|
||||
|
||||
<!--[[[end]]]
|
||||
-->
|
||||
|
||||
### Server generate-token command
|
||||
### Server generate-config
|
||||
|
||||
<!--
|
||||
.. [[[cog
|
||||
help(["server", "generate-config", "--help"])
|
||||
.. ]]] -->
|
||||
|
||||
```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.
|
||||
```
|
||||
|
||||
<!--[[[end]]]
|
||||
-->
|
||||
|
||||
### Server generate-token
|
||||
|
||||
<!--
|
||||
.. [[[cog
|
||||
|
@ -204,3 +266,202 @@ Options:
|
|||
|
||||
<!--[[[end]]]
|
||||
-->
|
||||
|
||||
### Server user management
|
||||
|
||||
To access Argos’ web interface, you need to create at least one user.
|
||||
|
||||
You can manage users only through CLI.
|
||||
|
||||
<!--
|
||||
.. [[[cog
|
||||
help(["server", "user", "--help"])
|
||||
.. ]]] -->
|
||||
|
||||
```man
|
||||
Usage: argos server user [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
User management
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
<!--[[[end]]]
|
||||
-->
|
||||
|
||||
#### Add user
|
||||
|
||||
<!--
|
||||
.. [[[cog
|
||||
help(["server", "user", "add", "--help"])
|
||||
.. ]]] -->
|
||||
|
||||
```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.
|
||||
```
|
||||
|
||||
<!--[[[end]]]
|
||||
-->
|
||||
|
||||
#### Change the password of a user
|
||||
|
||||
<!--
|
||||
.. [[[cog
|
||||
help(["server", "user", "change-password", "--help"])
|
||||
.. ]]] -->
|
||||
|
||||
```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.
|
||||
```
|
||||
|
||||
<!--[[[end]]]
|
||||
-->
|
||||
|
||||
#### Delete a user
|
||||
|
||||
<!--
|
||||
.. [[[cog
|
||||
help(["server", "user", "delete", "--help"])
|
||||
.. ]]] -->
|
||||
|
||||
```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.
|
||||
```
|
||||
|
||||
<!--[[[end]]]
|
||||
-->
|
||||
|
||||
#### 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.
|
||||
|
||||
<!--
|
||||
.. [[[cog
|
||||
help(["server", "user", "disable", "--help"])
|
||||
.. ]]] -->
|
||||
|
||||
```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.
|
||||
```
|
||||
|
||||
<!--[[[end]]]
|
||||
-->
|
||||
|
||||
#### 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.
|
||||
|
||||
<!--
|
||||
.. [[[cog
|
||||
help(["server", "user", "enable", "--help"])
|
||||
.. ]]] -->
|
||||
|
||||
```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.
|
||||
```
|
||||
|
||||
<!--[[[end]]]
|
||||
-->
|
||||
|
||||
#### List all users
|
||||
|
||||
Show all accounts, with their status (enabled or disabled).
|
||||
|
||||
<!--
|
||||
.. [[[cog
|
||||
help(["server", "user", "show", "--help"])
|
||||
.. ]]] -->
|
||||
|
||||
```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.
|
||||
```
|
||||
|
||||
<!--[[[end]]]
|
||||
-->
|
||||
|
||||
#### Test the password of a user
|
||||
|
||||
You can verify that you have the right password for a user with the following command:
|
||||
|
||||
<!--
|
||||
.. [[[cog
|
||||
help(["server", "user", "verify-password", "--help"])
|
||||
.. ]]] -->
|
||||
|
||||
```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.
|
||||
```
|
||||
|
||||
<!--[[[end]]]
|
||||
-->
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
# pylint: disable-msg=invalid-name,redefined-builtin
|
||||
import argos
|
||||
|
||||
project = "Argos"
|
||||
|
|
|
@ -1,56 +1,11 @@
|
|||
# Configuration
|
||||
|
||||
There are actually two configuration files: one for the service and one for the checks.
|
||||
|
||||
## 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.
|
||||
|
||||
#### 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:
|
||||
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 self-documented configuration file, which you can get with [`argos server generate-config`](cli.md#server-generate-config):
|
||||
|
||||
```{literalinclude} ../conf/config-example.yaml
|
||||
---
|
||||
caption: config.yaml
|
||||
caption: argos-config.yaml
|
||||
---
|
||||
|
||||
```
|
|
@ -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
|
||||
|
|
|
@ -4,6 +4,12 @@ Here are the systemd files that can be used to deploy the server and the agents.
|
|||
|
||||
## Agent
|
||||
|
||||
```{literalinclude} ../../conf/default-argos-agent
|
||||
---
|
||||
caption: /etc/default/argos-agent
|
||||
---
|
||||
```
|
||||
|
||||
```{literalinclude} ../../conf/systemd-agent.service
|
||||
---
|
||||
caption: /etc/systemd/system/argos-agent.service
|
||||
|
@ -12,6 +18,12 @@ caption: /etc/systemd/system/argos-agent.service
|
|||
|
||||
## Server
|
||||
|
||||
```{literalinclude} ../../conf/default-argos-server
|
||||
---
|
||||
caption: /etc/default/argos-server
|
||||
---
|
||||
```
|
||||
|
||||
```{literalinclude} ../../conf/systemd-server.service
|
||||
---
|
||||
caption: /etc/systemd/system/argos-server.service
|
||||
|
|
5
docs/developer/license.md
Normal file
5
docs/developer/license.md
Normal file
|
@ -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.
|
34
docs/developer/new-notification-way.md
Normal file
34
docs/developer/new-notification-way.md
Normal file
|
@ -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.
|
|
@ -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
|
||||
|
|
34
docs/faq.md
Normal file
34
docs/faq.md
Normal file
|
@ -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 gave 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 <https://framasoft.org/>.
|
||||
|
||||
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 <https://blog.notmyidea.org/> for more informations about him.
|
|
@ -37,6 +37,8 @@ installation/postgresql
|
|||
cli
|
||||
api
|
||||
changelog
|
||||
faq
|
||||
installation/tl-dr
|
||||
```
|
||||
|
||||
```{toctree}
|
||||
|
@ -61,9 +63,11 @@ developer/installation
|
|||
developer/overview
|
||||
developer/dependencies
|
||||
developer/new-check
|
||||
developer/new-notification-way
|
||||
developer/models
|
||||
developer/migrations
|
||||
developer/tests
|
||||
developer/release
|
||||
developer/license
|
||||
```
|
||||
|
||||
|
|
|
@ -1,10 +1,27 @@
|
|||
# Installation
|
||||
|
||||
NB: if you want a quick-installation guide, we [got you covered](tl-dr.md).
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.11+
|
||||
- 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
|
||||
|
||||
```bash
|
||||
|
@ -12,12 +29,19 @@ pip install argos-monitoring
|
|||
```
|
||||
|
||||
You may want to install Argos in a virtualenv:
|
||||
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
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:
|
||||
|
||||
```bash
|
||||
pip install "argos-monitoring[gunicorn]"
|
||||
```
|
||||
|
||||
## Install from sources
|
||||
|
||||
Once you got the source locally, create a virtualenv and install the dependencies:
|
||||
|
@ -28,27 +52,33 @@ source venv/bin/activate
|
|||
pip install -e .
|
||||
```
|
||||
|
||||
To install gunicorn, use `pip install -e ".[gunicorn]"` instead of `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 > argos-config.yaml
|
||||
```
|
||||
|
||||
You can read more about the configuration in the [configuration section](../configuration.md).
|
||||
|
||||
### Configure the server
|
||||
|
||||
Environment variables are used to configure the server. You can also put them in an `.env` file:
|
||||
|
||||
```{literalinclude} ../../conf/.env.example
|
||||
---
|
||||
caption: .env
|
||||
---
|
||||
For production, we suggest to put your config in `/etc/argos/config.yaml` and restricts the file’s permissions.
|
||||
As root:
|
||||
```bash
|
||||
mkdir /etc/argos
|
||||
chown argos: /etc/argos
|
||||
chmod 700 /etc/argos
|
||||
```
|
||||
|
||||
Please note that the only supported database engines are SQLite for development and PostgreSQL for production.
|
||||
Then, as `argos`:
|
||||
```bash
|
||||
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](postgresql.md) for production.
|
||||
|
||||
## Apply migrations to database
|
||||
|
||||
|
@ -60,7 +90,7 @@ argos server migrate
|
|||
|
||||
## 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:
|
||||
|
||||
|
@ -68,16 +98,6 @@ Populate the database with the tasks:
|
|||
argos server reload-config
|
||||
```
|
||||
|
||||
## Starting the server
|
||||
|
||||
Then you can start the server:
|
||||
|
||||
```bash
|
||||
argos server start
|
||||
```
|
||||
|
||||
The server reads the `yaml` file at startup, and populates the tasks queue with the checks defined in the configuration.
|
||||
|
||||
## Generating a token
|
||||
|
||||
The agent needs an authentication token to be able to communicate with the server.
|
||||
|
@ -95,10 +115,54 @@ service:
|
|||
- "auth-token"
|
||||
```
|
||||
|
||||
## Starting the server
|
||||
|
||||
Then you can start the server:
|
||||
|
||||
```bash
|
||||
argos server start
|
||||
```
|
||||
|
||||
This way to start the server is not suitable for production, use it only for developing or testing.
|
||||
|
||||
## Starting the server for production
|
||||
|
||||
For production, you can use [Gunicorn](https://gunicorn.org/) to start the server.
|
||||
|
||||
To install Gunicorn in the virtualenv, if you didn’t already install Argos that way:
|
||||
|
||||
```bash
|
||||
pip install "argos-monitoring[gunicorn]"
|
||||
```
|
||||
|
||||
To start the server:
|
||||
|
||||
```bash
|
||||
gunicorn "argos.server.main:get_application()" -k uvicorn.workers.UvicornWorker
|
||||
```
|
||||
|
||||
There is some gunicorn’s options that you should use:
|
||||
- `-w INT, --workers INT`: the number of worker processes for handling requests. Default is `1`.
|
||||
- `-b ADDRESS, --bind ADDRESS`: the socket to bind. Default is `127.0.0.1:8000`.
|
||||
- `--forwarded-allow-ips STRING`: front-end's IPs from which allowed to handle set secure headers as a comma-separated list. Default is `127.0.0.1`.
|
||||
|
||||
So, to start the server with 4 workers while listening to `127.0.0.1:8001`:
|
||||
|
||||
```bash
|
||||
gunicorn "argos.server.main:get_application()" -k uvicorn.workers.UvicornWorker -w 4 -b 127.0.0.1:8001
|
||||
```
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Running the agent
|
||||
|
||||
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
|
||||
argos agent http://localhost:8000 "auth-token"
|
||||
|
@ -106,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:
|
||||
|
||||
|
@ -115,3 +179,15 @@ Here is a crontab example, which will clean the db each hour:
|
|||
# Keeps 10 results per task, and remove tasks’ locks older than 1 hour
|
||||
7 * * * * argos server cleandb --max-results 10 --max-lock-seconds 3600
|
||||
```
|
||||
|
||||
## Watch the agents
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
*/5 * * * * argos server watch-agents --time-without-agent 10
|
||||
```
|
||||
|
||||
Check the documentation of the command with `argos server watch-agents --help`
|
||||
|
|
158
docs/installation/tl-dr.md
Normal file
158
docs/installation/tl-dr.md
Normal file
|
@ -0,0 +1,158 @@
|
|||
# TL;DR: fast installation instructions
|
||||
|
||||
You want to install Argos fast? Ok, here we go.
|
||||
|
||||
## For testing
|
||||
|
||||
This is for testing only!
|
||||
|
||||
```bash
|
||||
sudo apt install python3
|
||||
mkdir /tmp/argos
|
||||
cd /tmp/argos
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install argos-monitoring
|
||||
argos server generate-config |
|
||||
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)
|
||||
sed -e "s@# - secret_token@- $ARGOS_TOKEN@" -i argos-config.yaml
|
||||
echo "The agent token is $ARGOS_TOKEN"
|
||||
```
|
||||
|
||||
Edit `argos-config.yaml`.
|
||||
Add some real web sites to test.
|
||||
|
||||
Then:
|
||||
|
||||
```
|
||||
argos server reload-config
|
||||
argos server start --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
In another terminal:
|
||||
|
||||
```
|
||||
cd /tmp/argos
|
||||
source venv/bin/activate
|
||||
argos agent http://127.0.0.1:8000 the_generated_token
|
||||
```
|
||||
|
||||
Then go to `http://127.0.0.1:8000` or `http://the_IP_address_of_your_server:8000`.
|
||||
|
||||
## For production
|
||||
|
||||
```bash
|
||||
apt install python3 postgresql
|
||||
sudo -u postgres createuser -P argos
|
||||
sudo -u postgres createdb -O argos argos
|
||||
sudo -u postgres psql -c "ALTER DATABASE argos SET TIMEZONE TO 'UTC';"
|
||||
adduser --home /opt/argos --disabled-login --disabled-password --system argos
|
||||
|
||||
cd /opt/argos
|
||||
sudo -u argos python3 -m venv venv
|
||||
sudo -u argos bash -c 'source venv/bin/activate && pip install "argos-monitoring[gunicorn]"'
|
||||
|
||||
mkdir /etc/argos
|
||||
/opt/argos/venv/bin/argos server generate-config > /etc/argos/config.yaml
|
||||
|
||||
cat <<EOF > /etc/default/argos-server
|
||||
ARGOS_YAML_FILE="/etc/argos/config.yaml"
|
||||
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
|
||||
EOF
|
||||
|
||||
cat <<EOF > /etc/default/argos-agent
|
||||
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
|
||||
EOF
|
||||
|
||||
cat <<EOF > /etc/systemd/system/argos-server.service
|
||||
[Unit]
|
||||
Description=Argos server
|
||||
Documentation=https://argos-monitoring.framasoft.org/
|
||||
Requires=network.target postgresql.service
|
||||
After=network.target postgresql.service
|
||||
PartOf=postgresql.service
|
||||
|
||||
[Service]
|
||||
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
|
||||
EOF
|
||||
|
||||
cat <<EOF > /etc/systemd/system/argos-agent.service
|
||||
[Unit]
|
||||
Description=Argos agent
|
||||
Documentation=https://argos-monitoring.framasoft.org/
|
||||
Requires=network.target
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
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
|
||||
EOF
|
||||
|
||||
chown -R argos: /etc/default/argos-* /etc/argos/
|
||||
chmod 700 /etc/argos
|
||||
chmod 600 /etc/argos/config.yaml
|
||||
|
||||
systemctl daemon-reload
|
||||
```
|
||||
|
||||
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 :
|
||||
|
||||
```bash
|
||||
sudo -u argos /opt/argos/venv/bin/argos server generate-token
|
||||
```
|
||||
|
||||
Edit `/etc/default/argos-agent` to put the generated token in it and change the other settings to suit your needs.
|
||||
|
||||
Edit `/etc/argos/config.yaml` to configure Argos (don’t forget to add the generated token in it too).
|
||||
|
||||
Enable and start the server and the agent and make sure they works:
|
||||
|
||||
```bash
|
||||
systemctl enable --now argos-server.service argos-agent.service
|
||||
systemctl status argos-server.service argos-agent.service
|
||||
```
|
||||
|
||||
If all works well, you have to put some cron tasks in `argos` crontab:
|
||||
|
||||
```bash
|
||||
cat <<EOF | crontab -u argos -
|
||||
*/10 * * * * /opt/argos/venv/bin/argos server cleandb --max-lock-seconds 120 --max-results 1200
|
||||
*/10 * * * * /opt/argos/venv/bin/argos server watch-agents --time-without-agent 10
|
||||
EOF
|
||||
```
|
||||
|
||||
See the [this page](../deployment/nginx.md) for using Nginx as reverse proxy.
|
|
@ -22,11 +22,13 @@ classifiers = [
|
|||
|
||||
dependencies = [
|
||||
"alembic>=1.13.0,<1.14",
|
||||
"bcrypt>=4.1.3,<5",
|
||||
"click>=8.1,<9",
|
||||
"fastapi>=0.103,<0.104",
|
||||
"gunicorn>=21.2,<22",
|
||||
"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",
|
||||
|
@ -62,9 +64,12 @@ docs = [
|
|||
"sphinx>=7,<8",
|
||||
"sphinxcontrib-mermaid>=0.9,<1",
|
||||
]
|
||||
gunicorn = [
|
||||
"gunicorn>=21.2,<22",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://framasoft.frama.io/framaspace/argos/"
|
||||
homepage = "https://argos-monitoring.framasoft.org/"
|
||||
repository = "https://framagit.org/framasoft/framaspace/argos"
|
||||
"Funding" = "https://framasoft.org/en/#support"
|
||||
"Tracker" = "https://framagit.org/framasoft/framaspace/argos/-/issues"
|
||||
|
@ -94,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",
|
||||
]
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
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
|
||||
cookie_secret: "foo-bar-baz"
|
||||
frequency: "1m"
|
||||
alerts:
|
||||
ok:
|
||||
|
|
|
@ -6,7 +6,7 @@ from fastapi import FastAPI
|
|||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
os.environ["ARGOS_APP_ENV"] = "test"
|
||||
os.environ["ARGOS_YAML_FILE"] = "tests/config.yaml"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -45,10 +45,6 @@ def _create_app() -> FastAPI:
|
|||
)
|
||||
|
||||
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)
|
||||
asyncio.run(connect_to_db(app))
|
||||
|
|
150
tests/test_cli.py
Normal file
150
tests/test_cli.py
Normal file
|
@ -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"
|
|
@ -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,24 +160,73 @@ 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
|
||||
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=["", ""],
|
||||
|
@ -191,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",
|
||||
|
@ -212,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",
|
||||
|
@ -222,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
|
||||
|
||||
|
@ -232,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",
|
||||
|
@ -242,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
|
||||
|
||||
|
@ -252,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",
|
||||
|
@ -262,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
|
||||
|
||||
|
@ -272,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",
|
||||
|
@ -282,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
|
||||
|
||||
|
@ -292,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",
|
||||
|
@ -302,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
|
||||
|
|
Loading…
Reference in a new issue