mirror of
https://framagit.org/framasoft/framaspace/argos.git
synced 2025-04-28 09:52:38 +02:00
✨ — Allow to specify form data and headers for checks (fix #70)
This commit is contained in:
parent
9c8be94c20
commit
2ef999fa63
11 changed files with 137 additions and 17 deletions
|
@ -6,6 +6,7 @@
|
||||||
- ⚡ — Mutualize check requests (#68)
|
- ⚡ — Mutualize check requests (#68)
|
||||||
- ✨ — Ability to delay notification after X failures (#71)
|
- ✨ — Ability to delay notification after X failures (#71)
|
||||||
- 🐛 — Fix bug when changing IP version not removing tasks (#72)
|
- 🐛 — Fix bug when changing IP version not removing tasks (#72)
|
||||||
|
- ✨ — Allow to specify form data and headers for checks (#70)
|
||||||
|
|
||||||
## 0.6.1
|
## 0.6.1
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
|
from hashlib import md5
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
@ -57,7 +58,7 @@ class ArgosAgent: # pylint: disable-msg=too-many-instance-attributes
|
||||||
self._http_client = httpx.AsyncClient(headers=auth_header)
|
self._http_client = httpx.AsyncClient(headers=auth_header)
|
||||||
|
|
||||||
ua_header = {
|
ua_header = {
|
||||||
"User-Agent": f"Prout Argos Panoptes {VERSION} "
|
"User-Agent": f"Argos Panoptes {VERSION} "
|
||||||
"(about: https://argos-monitoring.framasoft.org/)",
|
"(about: https://argos-monitoring.framasoft.org/)",
|
||||||
}
|
}
|
||||||
self._http_client_v4 = httpx.AsyncClient(
|
self._http_client_v4 = httpx.AsyncClient(
|
||||||
|
@ -77,24 +78,59 @@ class ArgosAgent: # pylint: disable-msg=too-many-instance-attributes
|
||||||
await asyncio.sleep(self.wait_time)
|
await asyncio.sleep(self.wait_time)
|
||||||
|
|
||||||
async def _do_request(self, group: str, details: dict):
|
async def _do_request(self, group: str, details: dict):
|
||||||
|
headers = {}
|
||||||
|
if details["request_data"] is not None:
|
||||||
|
request_data = json.loads(details["request_data"])
|
||||||
|
if request_data["headers"] is not None:
|
||||||
|
headers = request_data["headers"]
|
||||||
|
|
||||||
|
if details["ip_version"] == "4":
|
||||||
|
http_client = self._http_client_v4
|
||||||
|
else:
|
||||||
|
http_client = self._http_client_v6
|
||||||
try:
|
try:
|
||||||
if details["ip_version"] == "4":
|
if details["request_data"] is None or request_data["data"] is None:
|
||||||
response = await self._http_client_v4.request( # type: ignore[union-attr]
|
response = await http_client.request( # type: ignore[union-attr]
|
||||||
method=details["method"], url=details["url"], timeout=60
|
method=details["method"],
|
||||||
|
url=details["url"],
|
||||||
|
headers=headers,
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
elif request_data["json"]:
|
||||||
|
response = await http_client.request( # type: ignore[union-attr]
|
||||||
|
method=details["method"],
|
||||||
|
url=details["url"],
|
||||||
|
headers=headers,
|
||||||
|
json=request_data["data"],
|
||||||
|
timeout=60,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
response = await self._http_client_v6.request( # type: ignore[union-attr]
|
response = await http_client.request( # type: ignore[union-attr]
|
||||||
method=details["method"], url=details["url"], timeout=60
|
method=details["method"],
|
||||||
|
url=details["url"],
|
||||||
|
headers=headers,
|
||||||
|
data=request_data["data"],
|
||||||
|
timeout=60,
|
||||||
)
|
)
|
||||||
except httpx.ReadError:
|
except httpx.ReadError:
|
||||||
sleep(1)
|
sleep(1)
|
||||||
if details["ip_version"] == "4":
|
if details["request_data"] is None or request_data["data"] is None:
|
||||||
response = await self._http_client_v4.request( # type: ignore[union-attr]
|
response = await http_client.request( # type: ignore[union-attr]
|
||||||
method=details["method"], url=details["url"], timeout=60
|
method=details["method"], url=details["url"], timeout=60
|
||||||
)
|
)
|
||||||
|
elif request_data["json"]:
|
||||||
|
response = await http_client.request( # type: ignore[union-attr]
|
||||||
|
method=details["method"],
|
||||||
|
url=details["url"],
|
||||||
|
json=request_data["data"],
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
response = await self._http_client_v6.request( # type: ignore[union-attr]
|
response = await http_client.request( # type: ignore[union-attr]
|
||||||
method=details["method"], url=details["url"], timeout=60
|
method=details["method"],
|
||||||
|
url=details["url"],
|
||||||
|
data=request_data["data"],
|
||||||
|
timeout=60,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._res_cache[group] = response
|
self._res_cache[group] = response
|
||||||
|
@ -135,14 +171,21 @@ class ArgosAgent: # pylint: disable-msg=too-many-instance-attributes
|
||||||
group = task.task_group
|
group = task.task_group
|
||||||
|
|
||||||
if task.check == "http-to-https":
|
if task.check == "http-to-https":
|
||||||
|
data = task.request_data
|
||||||
|
if data is None:
|
||||||
|
data = ""
|
||||||
url = str(httpx.URL(task.url).copy_with(scheme="http"))
|
url = str(httpx.URL(task.url).copy_with(scheme="http"))
|
||||||
group = f"{task.method}-{task.ip_version}-{url}"
|
group = (
|
||||||
|
f"{task.method}-{task.ip_version}-{url}-"
|
||||||
|
f"{md5(data.encode()).hexdigest()}"
|
||||||
|
)
|
||||||
_task["task_group"] = group
|
_task["task_group"] = group
|
||||||
|
|
||||||
req_groups[group] = {
|
req_groups[group] = {
|
||||||
"url": url,
|
"url": url,
|
||||||
"ip_version": task.ip_version,
|
"ip_version": task.ip_version,
|
||||||
"method": task.method,
|
"method": task.method,
|
||||||
|
"request_data": task.request_data,
|
||||||
}
|
}
|
||||||
|
|
||||||
requests = []
|
requests = []
|
||||||
|
|
|
@ -190,6 +190,17 @@ websites:
|
||||||
- 302
|
- 302
|
||||||
- 307
|
- 307
|
||||||
- path: "/admin/"
|
- path: "/admin/"
|
||||||
|
methode: "POST"
|
||||||
|
# Send form data in the request
|
||||||
|
request_data:
|
||||||
|
data:
|
||||||
|
login: "admin"
|
||||||
|
password: "my-password"
|
||||||
|
# To send data as JSON (optional, default is false):
|
||||||
|
is_json: true
|
||||||
|
# To send additional headers
|
||||||
|
headers:
|
||||||
|
Authorization: "Bearer foo-bar-baz"
|
||||||
checks:
|
checks:
|
||||||
# Check that the return HTTP status is one of those
|
# Check that the return HTTP status is one of those
|
||||||
# Similar to status-is, verify that you don’t mistyped it!
|
# Similar to status-is, verify that you don’t mistyped it!
|
||||||
|
|
|
@ -5,7 +5,7 @@ For database models, see argos.server.models.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from typing import Dict, List, Literal, Tuple
|
from typing import Any, Dict, List, Literal, Tuple
|
||||||
|
|
||||||
from durations_nlp import Duration
|
from durations_nlp import Duration
|
||||||
from pydantic import (
|
from pydantic import (
|
||||||
|
@ -18,7 +18,7 @@ from pydantic import (
|
||||||
PositiveInt,
|
PositiveInt,
|
||||||
field_validator,
|
field_validator,
|
||||||
)
|
)
|
||||||
from pydantic.functional_validators import BeforeValidator
|
from pydantic.functional_validators import AfterValidator, BeforeValidator
|
||||||
from pydantic.networks import UrlConstraints
|
from pydantic.networks import UrlConstraints
|
||||||
from pydantic_core import Url
|
from pydantic_core import Url
|
||||||
from typing_extensions import Annotated
|
from typing_extensions import Annotated
|
||||||
|
@ -104,9 +104,26 @@ def parse_checks(value):
|
||||||
return (name, expected)
|
return (name, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_request_data(value):
|
||||||
|
"""Turn form or JSON data into JSON string"""
|
||||||
|
|
||||||
|
return json.dumps(
|
||||||
|
{"data": value.data, "json": value.is_json, "headers": value.headers}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestData(BaseModel):
|
||||||
|
data: Any = None
|
||||||
|
is_json: bool = False
|
||||||
|
headers: Dict[str, str] | None = None
|
||||||
|
|
||||||
|
|
||||||
class WebsitePath(BaseModel):
|
class WebsitePath(BaseModel):
|
||||||
path: str
|
path: str
|
||||||
method: Method = "GET"
|
method: Method = "GET"
|
||||||
|
request_data: Annotated[
|
||||||
|
RequestData, AfterValidator(parse_request_data)
|
||||||
|
] | None = None
|
||||||
checks: List[
|
checks: List[
|
||||||
Annotated[
|
Annotated[
|
||||||
Tuple[str, str],
|
Tuple[str, str],
|
||||||
|
|
|
@ -22,6 +22,7 @@ class Task(BaseModel):
|
||||||
ip_version: IPVersion
|
ip_version: IPVersion
|
||||||
check: str
|
check: str
|
||||||
method: Method
|
method: Method
|
||||||
|
request_data: str | None
|
||||||
expected: str
|
expected: str
|
||||||
task_group: str
|
task_group: str
|
||||||
retry_before_notification: int
|
retry_before_notification: int
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
"""Add request data to tasks
|
||||||
|
|
||||||
|
Revision ID: 31255a412d63
|
||||||
|
Revises: 80a29f64f91c
|
||||||
|
Create Date: 2024-12-09 16:40:20.926138
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "31255a412d63"
|
||||||
|
down_revision: Union[str, None] = "80a29f64f91c"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
with op.batch_alter_table("tasks", schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column("request_data", sa.String(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
with op.batch_alter_table("tasks", schema=None) as batch_op:
|
||||||
|
batch_op.drop_column("request_data")
|
|
@ -1,6 +1,7 @@
|
||||||
"""Database models"""
|
"""Database models"""
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from hashlib import md5
|
||||||
from typing import List, Literal
|
from typing import List, Literal
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
|
@ -17,10 +18,14 @@ from argos.schemas.utils import IPVersion, Method
|
||||||
|
|
||||||
|
|
||||||
def compute_task_group(context) -> str:
|
def compute_task_group(context) -> str:
|
||||||
|
data = context.current_parameters["request_data"]
|
||||||
|
if data is None:
|
||||||
|
data = ""
|
||||||
return (
|
return (
|
||||||
f"{context.current_parameters['method']}-"
|
f"{context.current_parameters['method']}-"
|
||||||
f"{context.current_parameters['ip_version']}-"
|
f"{context.current_parameters['ip_version']}-"
|
||||||
f"{context.current_parameters['url']}"
|
f"{context.current_parameters['url']}-"
|
||||||
|
f"{md5(data.encode()).hexdigest()}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,6 +72,7 @@ class Task(Base):
|
||||||
),
|
),
|
||||||
insert_default="GET",
|
insert_default="GET",
|
||||||
)
|
)
|
||||||
|
request_data: Mapped[str] = mapped_column(nullable=True)
|
||||||
|
|
||||||
# Orchestration-related
|
# Orchestration-related
|
||||||
selected_by: Mapped[str] = mapped_column(nullable=True)
|
selected_by: Mapped[str] = mapped_column(nullable=True)
|
||||||
|
|
|
@ -256,6 +256,7 @@ async def update_from_config(db: Session, config: schemas.Config): # pylint: di
|
||||||
.filter(
|
.filter(
|
||||||
Task.url == url,
|
Task.url == url,
|
||||||
Task.method == p.method,
|
Task.method == p.method,
|
||||||
|
Task.request_data == p.request_data,
|
||||||
Task.check == check_key,
|
Task.check == check_key,
|
||||||
Task.expected == expected,
|
Task.expected == expected,
|
||||||
Task.ip_version == ip_version,
|
Task.ip_version == ip_version,
|
||||||
|
@ -300,7 +301,14 @@ async def update_from_config(db: Session, config: schemas.Config): # pylint: di
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
properties = (url, p.method, check_key, expected, ip_version)
|
properties = (
|
||||||
|
url,
|
||||||
|
p.method,
|
||||||
|
check_key,
|
||||||
|
expected,
|
||||||
|
ip_version,
|
||||||
|
p.request_data,
|
||||||
|
)
|
||||||
if properties not in unique_properties:
|
if properties not in unique_properties:
|
||||||
unique_properties.append(properties)
|
unique_properties.append(properties)
|
||||||
task = Task(
|
task = Task(
|
||||||
|
@ -308,6 +316,7 @@ async def update_from_config(db: Session, config: schemas.Config): # pylint: di
|
||||||
url=url,
|
url=url,
|
||||||
ip_version=ip_version,
|
ip_version=ip_version,
|
||||||
method=p.method,
|
method=p.method,
|
||||||
|
request_data=p.request_data,
|
||||||
check=check_key,
|
check=check_key,
|
||||||
expected=expected,
|
expected=expected,
|
||||||
frequency=frequency,
|
frequency=frequency,
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
---
|
||||||
general:
|
general:
|
||||||
db:
|
db:
|
||||||
# The database URL, as defined in SQLAlchemy docs : https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls
|
# 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"
|
url: "sqlite:////tmp/test-argos.db"
|
||||||
env: test
|
env: test
|
||||||
cookie_secret: "foo-bar-baz"
|
cookie_secret: "foo-bar-baz"
|
||||||
|
|
|
@ -37,6 +37,7 @@ def ssl_task(now):
|
||||||
domain="https://example.org",
|
domain="https://example.org",
|
||||||
ip_version="6",
|
ip_version="6",
|
||||||
method="GET",
|
method="GET",
|
||||||
|
request_data=None,
|
||||||
task_group="GET-6-https://example.org",
|
task_group="GET-6-https://example.org",
|
||||||
check="ssl-certificate-expiration",
|
check="ssl-certificate-expiration",
|
||||||
retry_before_notification=0,
|
retry_before_notification=0,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
---
|
||||||
- domain: "https://mypads.framapad.org"
|
- domain: "https://mypads.framapad.org"
|
||||||
paths:
|
paths:
|
||||||
- path: "/mypads/"
|
- path: "/mypads/"
|
||||||
checks:
|
checks:
|
||||||
- status-is: 200
|
- status-is: 200
|
||||||
- body-contains: '<div id= "mypads"></div>'
|
- body-contains: '<div id= "mypads"></div>'
|
||||||
|
|
Loading…
Reference in a new issue