Compare commits

..

No commits in common. "c1863033bccc50f1b0d7209fc26eb89672894163" and "a6904ba37034e36ceae3dc3efa6e376ff20070a4" have entirely different histories.

6 changed files with 29 additions and 147 deletions

View file

@ -1,69 +0,0 @@
{ lib }:
rec {
mkFqdn = _: cfg: cfg.networking.fqdn;
fromHive =
{
builder,
nodes,
excludes ? [ ],
}:
lib.mkMerge (
builtins.map (node: builder node nodes.${node}) (
lib.subtractLists excludes (builtins.attrNames nodes)
)
);
pingProbesFromHive =
{
mkHost,
nodes,
prefix ? "Ping ",
excludes ? [ ],
tags ? [ ],
}:
fromHive {
builder = (
node: module: {
monitors = {
${prefix + node} = {
type = "ping";
inherit tags;
hostname = mkHost node module.config;
};
};
}
);
inherit nodes excludes;
};
httpProbesFromConfig =
{
config,
excludes ? [ ],
prefix ? "",
type ? "keyword",
tags ? [ ],
}:
let
filter = k: v: !builtins.elem k excludes && v.globalRedirect == null;
in
{
monitors = lib.mapAttrs' (
vhostName: vhost:
let
hasSSL = vhost.onlySSL || vhost.enableSSL || vhost.addSSL || vhost.forceSSL;
serverName = if vhost.serverName != null then vhost.serverName else vhostName;
in
{
name = prefix + serverName;
value = {
inherit type;
inherit tags;
url = "http${lib.optionalString hasSSL "s"}://${serverName}";
method = "get";
};
}
) (lib.filterAttrs filter config.services.nginx.virtualHosts);
};
}

View file

@ -22,31 +22,29 @@ in
Extra arguments to use for executing `stateless-uptime-kuma`. Extra arguments to use for executing `stateless-uptime-kuma`.
''; '';
}; };
lib = lib.mkOption { type = lib.types.raw; };
probesConfig = { probesConfig = {
monitors = lib.mkOption { monitors = lib.mkOption {
type = with lib.types; attrsOf probesFormat.type; inherit (probesFormat) type;
default = { }; default = [ ];
}; };
tags = lib.mkOption { tags = lib.mkOption {
type = with lib.types; attrsOf probesFormat.type; inherit (probesFormat) type;
default = { }; default = [ ];
}; };
notifications = lib.mkOption { notifications = lib.mkOption {
type = with lib.types; attrsOf probesFormat.type; inherit (probesFormat) type;
default = { }; default = [ ];
}; };
}; };
}; };
config.statelessUptimeKuma = { config.statelessUptimeKuma = {
lib = import ../lib { inherit lib; };
build = { build = {
json = probesFormat.generate "probes.json" cfg.probesConfig; json = probesFormat.generate "probes.json" cfg.probesConfig;
script = pkgs.writeShellApplication { script = pkgs.writeShellApplication {
name = "deploy-uptime-kuma-probes"; name = "deploy-uptime-kuma-probes";
runtimeInputs = [ pkgs.statelessUptimeKuma ]; runtimeInputs = [ pkgs.statelessUptimeKuma ];
text = '' text = ''
stateless-uptime-kuma apply-json -f ${cfg.build.json} ${builtins.concatStringsSep " " cfg.extraFlags} stateless-uptime-kuma apply-json -f ${cfg.build.json} ${cfg.extraFlags}
''; '';
}; };
}; };

View file

@ -35,34 +35,19 @@ def cli():
help="Scrape keywords for http probe", help="Scrape keywords for http probe",
default=False, default=False,
) )
@click.option( def apply_json(file, scrape_http_keywords):
"--keywords-fallback/--no-keywords-fallback",
help="When scrapping keywords, fallback to http if no keywords found",
default=True,
)
@click.option(
"--no-autocreate-tags",
"-t",
is_flag=True,
help="Don't automatically create tags if not in tags section of input",
default=False,
)
def apply_json(file, scrape_http_keywords, no_autocreate_tags, keywords_fallback):
""" """
Apply json probes Apply json probes
""" """
logger.debug(
f"Flags value:\n - scrape_http_keywords: {scrape_http_keywords}\n - no_autocreate_tags: {no_autocreate_tags}\n - keywords_fallback: {keywords_fallback}"
)
with UptimeKumaApi("http://localhost:3001") as api: with UptimeKumaApi("http://localhost:3001") as api:
api.login("admin", "123456789a")
logging.debug("Reading json") logging.debug("Reading json")
data = json.load(file) data = json.load(file)
logging.debug("Parsing json") logging.debug("Parsing json")
tree = from_dict(api, data, not no_autocreate_tags) tree = from_dict(api, data)
if scrape_http_keywords: if scrape_http_keywords:
hydrate_http_probes(tree) hydrate_http_probes(tree)
logging.debug("Sync probes") logging.debug("Sync probes")
api.login("admin", "123456789a")
Manager(api, tree).process() Manager(api, tree).process()

View file

@ -8,10 +8,10 @@ from uptime_kuma_api import MonitorType
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def hydrate_http_probes(tree, excludes=[], fallback_to_http = True): def hydrate_http_probes(tree, excludes=[]):
for probe in tree.get("monitors", []): for probe in tree.get("monitors", []):
if "type" not in probe.kwargs: if "type" not in probe.kwargs:
logger.error(f"Fatal: probe {probe.name} must have a 'type' parameter") logger.error("Fatal: probes must have a 'type' parameter")
sys.exit(1) sys.exit(1)
if ( if (
probe.kwargs["type"] == MonitorType.KEYWORD probe.kwargs["type"] == MonitorType.KEYWORD
@ -28,20 +28,9 @@ def hydrate_http_probes(tree, excludes=[], fallback_to_http = True):
method = probe.kwargs["method"] method = probe.kwargs["method"]
headers = probe.kwargs.get("headers") headers = probe.kwargs.get("headers")
body = probe.kwargs.get("body") body = probe.kwargs.get("body")
req = requests.request( content = requests.request(method, url, headers=headers, data=body).text
method, url, headers=headers, data=body, allow_redirects=True m = re.search("<title>(.*?)</title>", content)
)
req.encoding = req.apparent_encoding
if req.status_code not in probe.get_status_codes():
logger.error(f"{probe.name} is not returning the right status code")
m = re.search("(?s)<title[^>]*>(.*?)</title>", req.text)
if m is None: if m is None:
logger.warn( logger.info(f"Didn't find keywords for probe {probe.name}, skipping")
f"Didn't find keywords for probe {probe.name}, falling back on classic HTTP probe" probe.kwargs["keyword"] = m.group(1)
) logger.debug(f"New keyword: {m.group(1)}")
logging.debug(req.text)
if fallback_to_http:
probe.kwargs["type"] = "http"
else:
probe.kwargs["keyword"] = m.group(1).strip()
logger.debug(f"New keyword: {m.group(1).strip()}")

View file

@ -17,33 +17,31 @@ def die_tag_format_error():
sys.exit(1) sys.exit(1)
def from_dict(api, tree, autocreate_tags=True): def from_dict(api, tree):
notif = tree.get("notifications", []) notif = tree.get("notifications", [])
indexed_notifications = { indexed_notifications = {n["name"]: Notification(api, **n) for n in notif}
name: Notification(api, name, **kwargs) for name, kwargs in notif.items()
}
tags = tree.get("tags", []) tags = tree.get("tags", [])
indexed_tags = {name: Tag(api, name, **kwargs) for name, kwargs in tags.items()} indexed_tags = {t["name"]: Tag(api, **t) for t in tags}
monitors = tree.get("monitors", []) monitors = tree.get("monitors", [])
indexed_monitors = {} indexed_monitors = {}
for monitor_name, monitor_kwargs in monitors.items(): for m in monitors:
associated_tags = [] associated_tags = []
for tag in monitor_kwargs.get("tags", []): for tag in m.get("tags", []):
if not isinstance(tag, dict) or "name" not in tag: if not isinstance(tag, dict) or "name" not in tag:
die_tag_format_error() die_tag_format_error()
try: try:
if autocreate_tags and tag["name"] not in indexed_tags:
indexed_tags[tag["name"]] = Tag(api, name=tag["name"])
associated_tags.append((indexed_tags[tag["name"]], tag.get("value"))) associated_tags.append((indexed_tags[tag["name"]], tag.get("value")))
except IndexError: except IndexError:
die_tag_format_error() die_tag_format_error()
monitor_kwargs["tags"] = associated_tags m["tags"] = associated_tags
associated_notifications = [ associated_notifications = [
indexed_notifications[notif] indexed_notifications[notif] for notif in m.get("notifications", [])
for notif in monitor_kwargs.get("notifications", [])
] ]
monitor_kwargs["notifications"] = associated_notifications m["notifications"] = associated_notifications
indexed_monitors[monitor_name] = Monitor(api, monitor_name, **monitor_kwargs) if "name" not in m:
logger.error("Fatal: All monitors must have a name")
sys.exit(1)
indexed_monitors[m["name"]] = Monitor(api, **m)
return { return {
"monitors": indexed_monitors.values(), "monitors": indexed_monitors.values(),
"tags": indexed_tags.values(), "tags": indexed_tags.values(),

View file

@ -3,7 +3,6 @@ Classes to make the needed operations to reach the specified state.
""" """
import logging import logging
import sys
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -86,24 +85,6 @@ class Monitor(Item):
self.notifications = notifications self.notifications = notifications
self.saved = False self.saved = False
def get_status_codes(self):
if "accepted_statuscodes" not in self.kwargs:
return set(range(200, 300))
accepted_statuscodes = set()
for codes in self.kwargs["accepted_statuscodes"]:
if "-" in codes:
c = codes.split("-")
if len(c) != 2:
logger.error(
f"Fatal: status codes for {self} must be in the format 'number' or 'number-number'"
)
sys.exit(1)
a, b = int(c[0]), int(c[1])
accepted_statuscodes.update(range(a, b))
else:
accepted_statuscodes.add(int(codes))
return accepted_statuscodes
def save(self): def save(self):
if self.saved: if self.saved:
return return