Merge branch 'simplify-schema' into 'main'

🗃💄🎨 — Lot of changes: DB, UI and some code improvements

Closes #27

See merge request framasoft/framaspace/argos!17
This commit is contained in:
Luc Didry 2024-03-21 14:49:02 +00:00
commit 017738ffd7
26 changed files with 430 additions and 2806 deletions

View file

@ -21,7 +21,7 @@ cog: ## Run cog, to integrate the CLI options to the docs.
tests: venv ## Run the tests
venv/bin/pytest
djlint: venv ## Format the templates
venv/bin/djlint --ignore=H030,H031 --lint argos/server/templates/*html
venv/bin/djlint --ignore=H030,H031,H006 --profile jinja --lint argos/server/templates/*html
pylint: venv ## Runs pylint on the code
venv/bin/pylint argos
lint: djlint pylint

View file

@ -0,0 +1,37 @@
"""Add severity to Task and add severity level UNKNOWN
Revision ID: e99bc35702c9
Revises: 7d480e6f1112
Create Date: 2024-02-28 14:14:22.519918
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'e99bc35702c9'
down_revision: Union[str, None] = '7d480e6f1112'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.execute("ALTER TYPE severity ADD VALUE 'unknown'")
op.add_column('tasks',
sa.Column('severity',
sa.Enum(
'ok',
'warning',
'critical',
'unknown',
name='severity'),
nullable=False))
op.add_column('tasks', sa.Column('last_severity_update', sa.DateTime(), nullable=True))
def downgrade() -> None:
op.drop_column('tasks', 'last_severity_update')
op.drop_column('tasks', 'severity')

View file

@ -24,6 +24,7 @@ class Severity:
OK = "ok"
WARNING = "warning"
CRITICAL = "critical"
UNKNOWN = "unknown"
# XXX We could name this Result, but is it could overlap with schemas.Result.
@ -115,13 +116,15 @@ class BaseCheck:
- All FAILUREs should be reported as CRITICAL
- All SUCCESS should be reported as OK
- All ERRORS should be reported as CRITICAL.
- All ERRORS should be reported as UNKNOWN.
This behaviour can be changed in each check, by defining the `finalize` method.
XXX Allow this to be tweaked by the config.
"""
if result.status in (Status.SUCCESS, Status.ERROR):
if result.status == Status.SUCCESS:
return result.status, Severity.OK
if result.status == Status.ERROR:
return result.status, Severity.UNKNOWN
if result.status == Status.FAILURE:
return result.status, Severity.CRITICAL
if result.status == Status.ON_CHECK:

View file

@ -71,7 +71,7 @@ def agent(server_url, auth, max_tasks, wait_time, log_level):
from argos.logging import logger
logger.setLevel(log_level)
agent_ = ArgosAgent(server_url, auth, max_tasks, wait_time)
agent_ = ArgosAgent(server, auth, max_tasks, wait_time)
asyncio.run(agent_.run())

View file

@ -18,7 +18,7 @@ from typing_extensions import Annotated
from argos.schemas.utils import string_to_duration
Severity = Literal["warning", "error", "critical"]
Severity = Literal["warning", "error", "critical", "unknown"]
def parse_threshold(value):

View file

@ -41,7 +41,7 @@ Status: {severity}
Time: {result.submitted_at}
Previous status: {old_severity}
See results of task on {request.url_for('get_task_results', task_id=task.id)}
See results of task on {request.url_for('get_task_results_view', task_id=task.id)}
"""
mail = f"""\
@ -103,7 +103,7 @@ Status: {severity}
Time: {result.submitted_at}
Previous status: {old_severity}
See results of task on {request.url_for('get_task_results', task_id=task.id)}
See results of task on {request.url_for('get_task_results_view', task_id=task.id)}
"""
payload = {'title': subject,

View file

@ -41,6 +41,12 @@ class Task(Base):
completed_at: Mapped[datetime] = mapped_column(nullable=True)
next_run: Mapped[datetime] = mapped_column(nullable=True)
severity: Mapped[Literal["ok", "warning", "critical", "unknown"]] = mapped_column(
Enum("ok", "warning", "critical", "unknown", name="severity"),
insert_default="unknown"
)
last_severity_update: Mapped[datetime] = mapped_column(nullable=True)
results: Mapped[List["Result"]] = relationship(back_populates="task")
def __str__(self):
@ -50,8 +56,10 @@ class Task(Base):
"""Returns a check instance for this specific task"""
return get_registered_check(self.check)
def set_times_and_deselect(self):
"""Removes the lock on task and set the time for the next run"""
def set_times_severity_and_deselect(self, severity, submitted_at):
"""Removes the lock on task, set its severity and set the time for the next run"""
self.severity = severity
self.last_severity_update = submitted_at
self.selected_by = None
self.selected_at = None
@ -73,13 +81,6 @@ class Task(Base):
return None
return self.last_result.status
@property
def severity(self):
"""Get severity of the task"""
if not self.last_result:
return None
return self.last_result.severity
class Result(Base):
"""There is multiple results per tasks.
@ -100,8 +101,8 @@ class Result(Base):
status: Mapped[Literal["success", "failure", "error", "on-check"]] = mapped_column(
Enum("success", "failure", "error", "on-check", name="status")
)
severity: Mapped[Literal["ok", "warning", "critical"]] = mapped_column(
Enum("ok", "warning", "critical", name="severity")
severity: Mapped[Literal["ok", "warning", "critical", "unknown"]] = mapped_column(
Enum("ok", "warning", "critical", "unknown", name="severity")
)
context: Mapped[dict] = mapped_column()

View file

@ -110,29 +110,28 @@ async def update_from_config(db: Session, config: schemas.Config):
async def get_severity_counts(db: Session) -> dict:
"""Get the severities (ok, warning, critical…) and their count"""
# Get the last result of each task
subquery = (
db.query(Result.task_id, func.max(Result.id).label("max_result_id")) # pylint: disable-msg=not-callable
.group_by(Result.task_id)
.subquery()
)
# Join this back to get full result rows, and group by status
query = (
db.query(Result.severity, func.count(Result.id).label("count")) # pylint: disable-msg=not-callable
.join(subquery, Result.id == subquery.columns.max_result_id)
.group_by(Result.severity)
db.query(Task.severity, func.count(Task.id).label("count")) # pylint: disable-msg=not-callable
.group_by(Task.severity)
)
# Execute the query and fetch the results
task_counts_by_severity = query.all()
counts_dict = dict(task_counts_by_severity)
for key in ("ok", "warning", "critical"):
for key in ("ok", "warning", "critical", "unknown"):
counts_dict.setdefault(key, 0)
return counts_dict
async def reschedule_all(db: Session):
"""Reschedule checks of all non OK tasks ASAP"""
db.query(Task) \
.filter(Task.severity.in_(['warning', 'critical', 'unknown'])) \
.update({Task.next_run: datetime.now() - timedelta(days=1)})
db.commit()
async def remove_old_results(db: Session, max_results: int):
tasks = db.query(Task).all()
deleted = 0

View file

@ -55,7 +55,7 @@ async def create_results(
check = task.get_check()
status, severity = await check.finalize(config, result, **result.context)
result.set_status(status, severity)
task.set_times_and_deselect()
task.set_times_severity_and_deselect(severity, result.submitted_at)
# Dont create an alert if the severity has not changed
if last_severity != severity:
@ -67,7 +67,40 @@ async def create_results(
return {"result_ids": [r.id for r in db_results]}
@route.get("/stats")
@route.post("/reschedule/all",
responses={
200: {
"content": {
"application/json": {
"example": {
"msg": "Non OK tasks reschuled"
}
}
}
}
}
)
async def reschedule_all(request: Request, db: Session = Depends(get_db)):
"""Reschedule checks of all non OK tasks ASAP"""
await queries.reschedule_all(db)
return {"msg": "Non OK tasks reschuled"}
@route.get("/stats",
responses={
200: {
"content": {
"application/json": {
"example": {
"upcoming_tasks_count":0,
"results_count":1993085,
"selected_tasks_count":1845
}
}
}
}
}
)
async def get_stats(db: Session = Depends(get_db)):
"""Get tasks statistics"""
return {
@ -77,7 +110,19 @@ async def get_stats(db: Session = Depends(get_db)):
}
@route.get("/severities")
@route.get("/severities",
responses={
200: {
"content": {
"application/json": {
"example": {
"ok":1541,"warning":0,"critical":0,"unknown":0
}
}
}
}
}
)
async def get_severity_counts(db: Session = Depends(get_db)):
"""Returns the number of results per severity"""
return await queries.get_severity_counts(db)

View file

@ -5,8 +5,8 @@ from urllib.parse import urlparse
from fastapi import APIRouter, Depends, Request
from fastapi.templating import Jinja2Templates
from sqlalchemy import desc
from sqlalchemy.orm import Session, aliased
from sqlalchemy import func
from sqlalchemy.orm import Session
from argos.schemas import Config
from argos.server import queries
@ -20,12 +20,12 @@ SEVERITY_LEVELS = {
"ok": 1,
"warning": 2,
"critical": 3,
"to-process": 4
"unknown": 4
}
@route.get("/")
async def get_severity_counts(
async def get_severity_counts_view(
request: Request,
db: Session = Depends(get_db),
refresh: bool = False,
@ -48,26 +48,21 @@ async def get_severity_counts(
)
@route.get("/details")
async def read_tasks(request: Request, db: Session = Depends(get_db)):
@route.get("/domains")
async def get_domains_view(request: Request, db: Session = Depends(get_db)):
"""Show all tasks and their current state"""
tasks = db.query(Task).all()
results = (
db.query(Task, Result)
.join(Result)
.distinct(Task.id)
.order_by(Task.id, desc(Result.submitted_at))
.all()
)
domains_severities = defaultdict(list)
domains_last_checks = defaultdict(list)
for task, result in results:
severity = result.severity or "to-process"
for task in tasks:
domain = urlparse(task.url).netloc
domains_severities[domain].append(severity)
domains_last_checks[domain].append(result.submitted_at)
domains_severities[domain].append(task.severity)
if task.last_severity_update is not None:
domains_last_checks[domain] = task.last_severity_update
else:
domains_last_checks[domain] = 'Waiting to be checked'
def _max_severity(severities):
return max(severities, key=SEVERITY_LEVELS.get)
@ -84,17 +79,16 @@ async def read_tasks(request: Request, db: Session = Depends(get_db)):
return 0
domains = [(key, _max_severity(value)) for key, value in domains_severities.items()]
last_checks = {key: max(value) for key, value in domains_last_checks.items()}
domains.sort(key=cmp_to_key(_cmp_domains))
agents = db.query(Result.agent_id).distinct().all()
return templates.TemplateResponse(
"details.html",
"domains.html",
{
"request": request,
"domains": domains,
"last_checks": last_checks,
"last_checks": domains_last_checks,
"total_task_count": len(tasks),
"agents": agents,
},
@ -102,18 +96,18 @@ async def read_tasks(request: Request, db: Session = Depends(get_db)):
@route.get("/domain/{domain}")
async def get_domain_tasks(
async def get_domain_tasks_view(
request: Request, domain: str, db: Session = Depends(get_db)
):
"""Show all tasks attached to a domain"""
tasks = db.query(Task).filter(Task.domain.contains(domain)).all() # type: ignore[attr-defined]
tasks = db.query(Task).filter(Task.domain.contains(f'//{domain}')).all()
return templates.TemplateResponse(
"domain.html", {"request": request, "domain": domain, "tasks": tasks}
)
@route.get("/result/{result_id}")
async def get_result(request: Request, result_id: int, db: Session = Depends(get_db)):
async def get_result_view(request: Request, result_id: int, db: Session = Depends(get_db)):
"""Show the details of a result"""
result = db.query(Result).get(result_id)
return templates.TemplateResponse(
@ -122,7 +116,7 @@ async def get_result(request: Request, result_id: int, db: Session = Depends(get
@route.get("/task/{task_id}/results")
async def get_task_results(
async def get_task_results_view(
request: Request,
task_id: int,
db: Session = Depends(get_db),
@ -149,17 +143,11 @@ async def get_task_results(
@route.get("/agents")
async def get_agents(request: Request, db: Session = Depends(get_db)):
async def get_agents_view(request: Request, db: Session = Depends(get_db)):
"""Show argos agents and the last time the server saw them"""
t1 = aliased(Result, name="t1")
t2 = aliased(Result, name="t2")
last_seen = (
db.query(t1)
.outerjoin(
t2, (t1.agent_id == t2.agent_id) & (t1.submitted_at < t2.submitted_at)
)
.filter(t2.agent_id.is_(None))
db.query(Result.agent_id, func.max(Result.submitted_at).label('submitted_at'))
.group_by(Result.agent_id)
.all()
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

File diff suppressed because it is too large Load diff

5
argos/server/static/pico.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,9 +1,21 @@
@import url("pico.css");
@import url("pico.min.css");
code {
white-space: pre-wrap;
}
body > header,
body > main {
padding: 0 !important;
}
#title {
margin-bottom: 0;
}
h2 {
margin-bottom: calc(var(--typography-spacing-vertical) * 0.5);
}
.grid-index {
font-size: 2rem;
}
@ -14,6 +26,17 @@ code {
.grid-index article {
margin-top: 0;
margin-bottom: 1rem;
padding-bottom: calc(var(--block-spacing-vertical) * 0.7);
}
.grid-index article > header {
margin-bottom: calc(var(--block-spacing-vertical) * 0.7);
}
label[for="select-status"] {
display: inline-block;
}
#select-status {
width: initial;
}
.inline-label {

View file

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}<h2>Agents</h2>{% endblock %}
{% block title %}<h2>Agents</h2>{% endblock title %}
{% block content %}
<table role="grid">
<thead>
@ -17,4 +17,4 @@
{% endfor %}
</tbody>
</table>
{% endblock %}
{% endblock content %}

View file

@ -3,25 +3,69 @@
<head>
<meta charset="utf-8">
<title>Argos</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<link rel="shortcut icon"
href="{{ url_for('static', path='/logo.png') }}">
<meta name="viewport"
content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible"
content="IE=Edge">
{% if refresh %}
<meta http-equiv="refresh"
content="{{ delay }}">
{% endif %}
<link href="{{ url_for('static', path='/styles.css') }}"
rel="stylesheet">
<link rel="stylesheet"
href="{{ url_for('static', path='/styles.css') }}">
</head>
<body>
<header class="container">
<nav>
<ul>
<li>
<img src="{{ url_for('static', path='/logo-64.png') }}"
width="64"
height="64"
alt="">
</li>
<li>
<h1 id="title">Argos monitoring</h1>
</li>
</ul>
<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>
</ul>
</nav>
</header>
<main class="container">
<dialog id="msg"></dialog>
<div id="header">
{% block title %}
<a href="/"><h1 id="title">Argos monitoring</h1></a>
{% endblock %}
{% endblock title %}
</div>
<div id="content">
{% block content %}
{% endblock %}
{% endblock content %}
</div>
</main>
<footer class="text-center">
@ -34,5 +78,21 @@
or
<a href="{{ url_for('get_severity_counts_view') }}redoc">Redoc</a>
</footer>
<script>
async function rescheduleAll() {
const response = await fetch('{{ url_for("reschedule_all") }}', {method: 'POST'});
const json = await response.json();
const dialog = document.getElementById('msg');
dialog.innerText = json.msg;
dialog.setAttribute('open', '');
setTimeout(() => {
dialog.removeAttribute('open');
}, 1500);
}
document.getElementById('reschedule-all').addEventListener('click', event => {
event.preventDefault();
rescheduleAll();
});
</script>
</body>
</html>

View file

@ -1,33 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div id="domains" class="frame">
<p>
{{ domains | length}} domains,
{{ total_task_count }} tasks,
<a href="/agents">{{ agents | length }} agent{% if agents | length > 1 %}s{% endif %}</a>
</p>
<table id="domains-list" role="grid">
<thead>
<tr>
<th>Domain</th>
<th class="today">Current status</th>
<th>Last check</th>
</tr>
</thead>
<tbody id="domains-body">
{% for (domain, status) in domains %}
<tr>
<td>
<a href="/domain/{{ domain }}">{{ domain }}</a>
</td>
<td class="status highlight">
{% if status == "ok" %}✅ OK {% elif statuts == "warning"%}⚠ Warning{% elif status == "critical"%}❌ Critical{% elif status == "to-process" %}⏱︎ Waiting for the jobs{% endif %}
</td>
<td>{{ last_checks.get(domain) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}<h2>{{ domain }}</h2>{% endblock %}
{% block title %}<h2>{{ domain }}</h2>{% endblock title %}
{% block content %}
<div id="domains" class="frame">
<table id="domains-list" role="grid">
@ -20,12 +20,27 @@
<td>{{ task.check }}</td>
<td>{{ task.expected }}</td>
<td class="status highlight">
<a data-tooltip="Completed at {{ task.completed_at }}" href="/result/{{ task.last_result.id }}">{% if task.status == "success" %}✅ Success {% elif task.status == "error"%}⚠ Error{% elif task.status == "failure"%}❌ Failure{% endif %}</a>
{% if task.status %}
<a data-tooltip="Completed at {{ task.completed_at }}"
href="{{ url_for('get_result_view', result_id=task.last_result.id) }}">
{% if task.status == "success" %}
✅ Success
{% elif task.status == "error" %}
⚠ Error
{% elif task.status == "failure" %}
❌ Failure
{% elif task.status == "unknown" %}
❔ Unknown
{% endif %}
</a>
{% else %}
Waiting to be checked
{% endif %}
</td>
<td><a href="/task/{{task.id}}/results">view all</a></td>
<td><a href="{{ url_for('get_task_results_view', task_id=task.id) }}">view all</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
{% endblock content %}

View file

@ -0,0 +1,79 @@
{% extends "base.html" %}
{% block title %}<h2>Tasks list</h2>{% endblock title %}
{% block content %}
<div id="domains" class="frame">
<nav>
<ul>
<li>
{{ domains | length }} domains,
{{ total_task_count }} tasks,
<a href="{{ url_for('get_agents_view') }}">
{{ agents | length }} agent{{ 's' if agents | length > 1 }}
</a>
</li>
</ul>
<ul>
<li>
<label for="select-status">Show domains with status:</label>
<select id="select-status">
<option value="all">All</option>
<option value="ok">✅ OK</option>
<option value="warning">⚠️ Warning</option>
<option value="critical">❌ Critical</option>
<option value="unknown">❔ Unknown</option>
</select>
</li>
</ul>
</nav>
<table id="domains-list" role="grid">
<thead>
<tr>
<th>Domain</th>
<th class="today">Current status</th>
<th>Last check</th>
</tr>
</thead>
<tbody id="domains-body">
{% for (domain, status) in domains %}
<tr data-status={{ status }}>
<td>
<a href="{{ url_for('get_domain_tasks_view', domain=domain) }}">
{{ domain }}
</a>
</td>
<td class="status highlight">
{% if status == "ok" %}
✅ OK
{% elif status == "warning" %}
⚠️ Warning
{% elif status == "critical" %}
❌ Critical
{% elif status == "unknown" %}
❔ Unknown
{% endif %}
</td>
<td>{{ last_checks.get(domain) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<script>
document.getElementById('select-status').addEventListener('change', (e) => {
if (e.currentTarget.value === 'all') {
document.querySelectorAll('[data-status]').forEach((item) => {
item.style.display = null;
})
} else {
document.querySelectorAll('[data-status]').forEach((item) => {
if (item.dataset.status === e.currentTarget.value) {
item.style.display = null;
} else {
item.style.display = 'none';
}
})
}
});
</script>
{% endblock content %}

View file

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}<h2>Dashboard</h2>{% endblock %}
{% block title %}<h2>Dashboard</h2>{% endblock title %}
{% block content %}
<div id="domains" class="frame">
<nav>
@ -44,19 +44,19 @@
<div class="grid grid-index">
<article>
<header title="Unknown"></header>
<span id="sev-unknown">{{ counts_dict['unknown'] }}</span>
{{ counts_dict['unknown'] }}
</article>
<article>
<header title="OK"></header>
<span id="sev-ok">{{ counts_dict['ok'] }}</span>
{{ counts_dict['ok'] }}
</article>
<article>
<header title="Warning">⚠️</header>
<span id="sev-warning">{{ counts_dict['warning'] }}</span>
{{ counts_dict['warning'] }}
</article>
<article>
<header title="Critical"></header>
<span id="sev-critical">{{ counts_dict['critical'] }}</span>
{{ counts_dict['critical'] }}
</article>
</div>
<p class="text-center">
@ -68,4 +68,4 @@
</p>
</div>
</div>
{% endblock %}
{% endblock content %}

View file

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}<h2>{{ result }}</h2>{% endblock %}
{% block title %}<h2>{{ result }}</h2>{% endblock title %}
{% block content %}
<dl>
<dt>Task</dt>
@ -13,4 +13,4 @@
<dt>Context</dt>
<dd>{{ result.context }}</dd>
</dl>
{% endblock %}
{% endblock content %}

View file

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}<h2>{{ task }}</h2>{% endblock %}
{% block title %}<h2>{{ task }}</h2>{% endblock title %}
{% block content %}
<code>{{ description }}</code>
<table role="grid">
@ -23,4 +23,4 @@
</tbody>
</table>
{% endblock %}
{% endblock content %}

View file

@ -8,6 +8,7 @@
- [Jinja](https://jinja.palletsprojects.com/) is handling the templating;
- [Pydantic](https://pydantic.dev/) is useful to ensure the data matches our expectactions;
- [SQLAlchemy](https://www.sqlalchemy.org/) is the ORM we use, to connect to our database and issue queries;
- [Alembic](https://alembic.sqlalchemy.org) is used for DB migrations;
- [Tenacity](https://github.com/jd/tenacity) a small utility to retry a function in case an error occured;
- [Uvicorn](https://www.uvicorn.org/) is the tool used to run our server.

View file

@ -123,6 +123,21 @@ async def test_update_from_config_db_updates_existing_tasks(db, empty_config, ta
assert db.query(Task).count() == 1
@pytest.mark.asyncio
async def test_reschedule_all(db, ten_tasks, ten_warning_tasks, ten_critical_tasks, ten_ok_tasks):
assert db.query(Task).count() == 40
assert db.query(Task).filter(Task.severity == "unknown").count() == 10
assert db.query(Task).filter(Task.severity == "warning").count() == 10
assert db.query(Task).filter(Task.severity == "critical").count() == 10
assert db.query(Task).filter(Task.severity == "ok").count() == 10
one_hour_ago = datetime.now() - timedelta(hours=1)
assert db.query(Task).filter(Task.next_run <= one_hour_ago).count() == 0
await queries.reschedule_all(db)
assert db.query(Task).filter(Task.next_run <= one_hour_ago).count() == 30
@pytest.fixture
def task(db):
task = Task(
@ -215,3 +230,63 @@ def ten_tasks(db):
tasks.append(task)
db.commit()
return tasks
@pytest.fixture
def ten_warning_tasks(db):
now = datetime.now()
tasks = []
for i in range(10):
task = Task(
url="https://www.example.com",
domain="example.com",
check="body-contains",
expected="foo",
frequency=1,
next_run=now,
severity="warning"
)
db.add(task)
tasks.append(task)
db.commit()
return tasks
@pytest.fixture
def ten_critical_tasks(db):
now = datetime.now()
tasks = []
for i in range(10):
task = Task(
url="https://www.example.com",
domain="example.com",
check="body-contains",
expected="foo",
frequency=1,
next_run=now,
severity="critical"
)
db.add(task)
tasks.append(task)
db.commit()
return tasks
@pytest.fixture
def ten_ok_tasks(db):
now = datetime.now()
tasks = []
for i in range(10):
task = Task(
url="https://www.example.com",
domain="example.com",
check="body-contains",
expected="foo",
frequency=1,
next_run=now,
severity="ok"
)
db.add(task)
tasks.append(task)
db.commit()
return tasks