wip: nixpkgs versions + infra network + monitoring

Signed-off-by: Jeltz <jeltz@federez.net>
This commit is contained in:
jeltz 2025-04-05 17:33:42 +02:00
parent 01b5a0fe25
commit a64b34810d
Signed by: jeltz
GPG key ID: 800882B66C0C3326
24 changed files with 1363 additions and 513 deletions

23
pkgs/alertbot/default.nix Normal file
View file

@ -0,0 +1,23 @@
{
lib,
python3,
}:
python3.pkgs.buildPythonApplication rec {
pname = "alertbot";
version = "1.0.0";
pyproject = true;
disabled = python3.pythonOlder "3.12";
src = ./src;
build-system = [ python3.pkgs.hatchling ];
dependencies = with python3.pkgs; [ pydantic aiohttp matrix-nio jinja2 ];
meta = {
description = "Alertmanager Matrix Bot";
license = lib.licenses.agpl3Only;
};
}

View file

@ -0,0 +1 @@
# Alertbot

View file

@ -0,0 +1,83 @@
[project]
name = "alertbot"
version = "1.0.0"
description = "Alertmanager Matrix Bot"
readme = "README.md"
requires-python = ">=3.13"
license = "AGPL-3.0"
authors = [
{ name = "Tom Barthe", email = "tba@federez.net" },
]
classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: CPython",
]
dependencies = [
"aiohttp",
"pydantic >= 2.0.0",
"matrix-nio",
"jinja2",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project.scripts]
alertbot = "alertbot.__main__:main"
[tool.hatch.build.targets.wheel]
packages = ["src/alertbot"]
[tool.mypy]
strict = true
[tool.ruff]
line-length = 79
[tool.ruff.lint]
select = [
"F", # Pyflakes
"E", # pycodestyle errors
"W", # pycodestyle warnings
"I", # isort
"N", # pep8-naming
# "D", # pydocstyle
"UP", # pyupgrade
"YTT", # flake8-2020
"ANN", # flake8-annotations
"ASYNC", # flake8-async
"S", # flake8-bandit
"BLE", # flake8-blind-except
"A", # flake8-builtins
"C4", # flake8-comprehensions
"DTZ", # flake8-datetimez
"LOG", # flak8-logging
"G", # flak8-logging-format
"INP", # flak8-no-pep420
"PIE", # flak8-pie
"PYI", # flak8-pyi
"Q", # flak8-quotes
"RSE", # flake8-raise
"RET", # flake8-return
"SLF", # flake8-self
"SLOT", # flake8-slots
"SIM", # flake8-simplify
"TID", # flake8-tidy-imports
"ARG", # flake8-unused-arguments
"PTH", # flake8-use-pathlib
#"TD", # flake8-todos
"FIX", # flake8-fixme
"ERA", # eradicate
"PLC", # Pylint convention
"PLE", # Pylint error
"PLR", # Pylint refactor
"PLW", # Pylint warning
#"TRY", # tryceratops
"FLY", # flynt
"PERF", # Perflint
"FURB", # refurb
"RUF", # Ruff
]
ignore = []

View file

@ -0,0 +1,180 @@
import logging
from argparse import ArgumentParser
from asyncio import CancelledError, Queue, create_task
from contextlib import asynccontextmanager, suppress
from functools import cache
from os import environ
from pathlib import Path
from tomllib import load
from typing import Any
from aiohttp.web import AppKey, Application, Request, Response, post, run_app
from jinja2 import Environment
from nio import AsyncClient
from pydantic import BaseModel
TEMPLATE_TEXT = (
"{% if status == 'resolved' %}"
"\x02\x0303RÉSOLU\x03\x02 "
"{% elif labels.severity == 'critical' %}"
"\x02\x0304CRITIQUE\x03\x02 "
"{% elif labels.severity == 'warning' %}"
"\x02\x0307ATTENTION\x03\x02 "
"{% endif %}"
"\x02{{ labels.alertname }}\x02"
"{% if labels.instance is defined %} {{ labels.instance }}{% endif %}"
"{% if annotations.summary is defined %}"
"\n{{ annotations.summary }}"
"{% else %}"
"{% for key, value in annotations.items() %}"
"\n \x02{{ key }} :\x02 {{ value }}"
"{% endfor %}"
"{% endif %}"
)
TEMPLATE_HTML = (
"{% if status == 'resolved' %}"
"<b><font color='green'>RÉSOLU</font></b> "
"{% elif labels.severity == 'critical' %}"
"@room <b><font color='red'>CRITIQUE</font></b> "
"{% elif labels.severity == 'warning' %}"
"<b><font color='orange'>ATTENTION</font></b> "
"{% endif %}"
"<b>{{ labels.alertname }}</b>"
"{% if labels.instance is defined %} {{ labels.instance }}{% endif %}"
"{% if annotations.summary is defined %}"
"<br/><blockquote>{{ annotations.summary }}</blockquote>"
"{% else %}"
"<br/>"
"<blockquote>"
"{% for key, value in annotations.items() %}"
"<b>{{ key | capitalize }}&nbsp;:</b> {{ value }}<br/>"
"{% endfor %}"
"</ul>"
"</blockquote>"
"{% endif %}"
)
class MatrixConfig(BaseModel):
homeserver: str
user: str
password_cred: str
room_id: str
class Config(BaseModel):
matrix: MatrixConfig
listen_port: int
alert_queue = AppKey("alert_queue", Queue[Any])
config = AppKey("config", Config)
@cache
def read_cred(name: str) -> str:
creds_dir = Path(environ["CREDENTIALS_DIRECTORY"])
return (creds_dir / name).read_text()
async def handle_webhook(request: Request) -> Response:
message = await request.json()
alerts = message.get("alerts", [])
logging.info("Incoming message received: %s", message)
for alert in alerts:
await request.app[alert_queue].put(alert)
return Response()
async def post_alerts(
client: AsyncClient, queue: Queue[Any], room_id: str
) -> None:
env = Environment(autoescape=True)
text = env.from_string(TEMPLATE_TEXT)
html = env.from_string(TEMPLATE_HTML)
while True:
alert = await queue.get()
logging.info("Posting alert: %s", alert)
try:
await client.room_send(
room_id,
message_type="m.room.message",
content={
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"body": text.render(**alert),
"formatted_body": html.render(**alert),
},
)
except Exception:
logging.exception("Error while posting alert")
@asynccontextmanager
async def make_matrix_client(
homeserver: str, user: str, password: str, device_name: str
) -> AsyncClient:
client = AsyncClient(homeserver, user)
try:
logging.info("Logging in to %s as %s", homeserver, user)
await client.login(password, device_name=device_name)
yield client
finally:
logging.info("Closing matrix client session")
await client.close()
async def message_ctx_cleanup(app: Application) -> None:
homeserver = app[config].matrix.homeserver
user = app[config].matrix.user
password = read_cred(app[config].matrix.password_cred)
queue = Queue()
async with make_matrix_client(
homeserver, user, password, "Alertbot"
) as client:
task = create_task(
post_alerts(client, queue, app[config].matrix.room_id)
)
app[alert_queue] = queue
logging.info("Post alerts task created")
yield
logging.info("Cancelling post alert task")
task.cancel()
with suppress(CancelledError):
await task
def main() -> None:
logging.basicConfig(level=logging.INFO)
parser = ArgumentParser()
parser.add_argument(
"-c",
"--config",
type=Path,
default="config.toml",
)
args = parser.parse_args()
app = Application()
with args.config.open("rb") as f:
app[config] = Config.model_validate(load(f))
app.add_routes([post("/webhook", handle_webhook)])
app.cleanup_ctx.append(message_ctx_cleanup)
port = app[config].listen_port
logging.info("Starting Alertbot on port %s", port)
run_app(app, port=port, handler_cancellation=True)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,20 @@
--- a/package-lock.json 2025-02-16 07:50:02.223758771 +0100
+++ b/package-lock.json 2025-02-16 07:50:54.208768359 +0100
@@ -57,7 +57,7 @@
"process": "^0.11.10",
"prop-types": "^15.8.1",
"qs": "^6.11.0",
- "qtip2": "git+https://indico@github.com/indico/qTip2.git#8951e5538a5c0833021b2d2b5d8a587a2c24faae",
+ "qtip2": "file://@qTip2Tarball@",
"rc-time-picker": "^3.7.3",
"react": "^17.0.2",
"react-charts": "2.0.0-beta.7",
@@ -14265,7 +14265,7 @@
},
"node_modules/qtip2": {
"version": "3.0.3",
- "resolved": "git+https://indico@github.com/indico/qTip2.git#8951e5538a5c0833021b2d2b5d8a587a2c24faae",
+ "resolved": "file://@qTip2Tarball@",
"integrity": "sha512-U/oUhSv0FpWevgmJFbv4g2+Gl4HKcl4MmnlRSbuKVlgD+fu77Pzstw2FMOwKQMwXYmjiYJ6cMn638s6WiGuRqA==",
"dependencies": {
"imagesloaded": ">=3.0.0",