Seed Uptime Kuma monitors
This commit is contained in:
parent
7beb0778c9
commit
6070493b1b
|
|
@ -282,6 +282,9 @@ continue resolving if the Unbound container stops. Managed adlists live in
|
|||
`infra/rpi-services/adlists.txt`; the bootstrap inserts those lists without
|
||||
deleting manually added Pi-hole lists. If `PIHOLE_WEBPASSWORD` is not provided,
|
||||
the bootstrap generates one and keeps it in `/opt/homelab-rpi-services/.env`.
|
||||
Uptime Kuma monitor seeds live in
|
||||
`infra/rpi-services/uptime-kuma-monitors.json` and are applied after the first
|
||||
Kuma user exists.
|
||||
`./jeannie status` and `./jeannie doctor-rpi` test Pi-hole DNS,
|
||||
direct Unbound DNS, public fallback resolver readiness, Uptime Kuma HTTP, and
|
||||
whether Docker is currently using the NVMe path or `/var/lib/docker`.
|
||||
|
|
|
|||
|
|
@ -282,6 +282,9 @@ continue resolving if the Unbound container stops. Managed adlists live in
|
|||
`infra/rpi-services/adlists.txt`; the bootstrap inserts those lists without
|
||||
deleting manually added Pi-hole lists. If `PIHOLE_WEBPASSWORD` is not provided,
|
||||
the bootstrap generates one and keeps it in `/opt/homelab-rpi-services/.env`.
|
||||
Uptime Kuma monitor seeds live in
|
||||
`infra/rpi-services/uptime-kuma-monitors.json` and are applied after the first
|
||||
Kuma user exists.
|
||||
`./{{ main_script }} status` and `./{{ main_script }} doctor-rpi` test Pi-hole DNS,
|
||||
direct Unbound DNS, public fallback resolver readiness, Uptime Kuma HTTP, and
|
||||
whether Docker is currently using the NVMe path or `/var/lib/docker`.
|
||||
|
|
|
|||
11
homelab.yml
11
homelab.yml
|
|
@ -90,6 +90,17 @@ services:
|
|||
pihole_web_port: 8081
|
||||
uptime_kuma_port: 3001
|
||||
unbound_container_port: 53
|
||||
uptime_kuma_seed_file: infra/rpi-services/uptime-kuma-monitors.json
|
||||
arr:
|
||||
host: debian
|
||||
prowlarr_port: 9696
|
||||
radarr_port: 7878
|
||||
sonarr_port: 8989
|
||||
qbittorrent_port: 8080
|
||||
kapowarr_port: 5656
|
||||
suwayomi_port: 4567
|
||||
maintainerr_port: 6246
|
||||
flaresolverr_port: 8191
|
||||
traefik:
|
||||
host: kubernetes
|
||||
load_balancer_ip: 192.168.100.240
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ Kubernetes cluster:
|
|||
- Pi-hole on DNS port `53` and web port `8081`
|
||||
- Unbound as Pi-hole's first upstream resolver
|
||||
- Uptime Kuma on port `3001`
|
||||
- Uptime Kuma monitor seeds for public edge, Gitea, Traefik, RPi DNS, Pi-hole,
|
||||
and Debian-host ARR services
|
||||
|
||||
Pi-hole also keeps public fallback upstreams in its config, so DNS can continue
|
||||
to resolve through `1.1.1.1` and `9.9.9.9` if the Unbound container stops.
|
||||
|
|
@ -39,3 +41,8 @@ it in `/opt/homelab-rpi-services/.env` for future runs.
|
|||
|
||||
Managed Pi-hole adlists are stored in `adlists.txt`. The bootstrap inserts them
|
||||
without deleting existing manual Pi-hole lists.
|
||||
|
||||
Managed Uptime Kuma monitors are stored in `uptime-kuma-monitors.json`. The
|
||||
bootstrap seeds them after the Uptime Kuma database exists. If Uptime Kuma has
|
||||
not completed initial admin setup yet, the seed step skips cleanly; rerun
|
||||
`./jeannie rpi-services` after creating the first user.
|
||||
|
|
|
|||
|
|
@ -207,6 +207,35 @@ PY
|
|||
sudo docker exec "${pihole_container}" pihole -g
|
||||
}
|
||||
|
||||
seed_uptime_kuma_monitors() {
|
||||
local uptime_kuma_container="${UPTIME_KUMA_CONTAINER_NAME:-homelab-uptime-kuma}"
|
||||
local monitor_file="${INSTALL_DIR}/uptime-kuma-monitors.json"
|
||||
local seed_script="${INSTALL_DIR}/seed-uptime-kuma.js"
|
||||
local elapsed=0
|
||||
|
||||
if [[ ! -s "${monitor_file}" || ! -s "${seed_script}" ]]; then
|
||||
echo "Uptime Kuma monitor seed files are missing; skipping monitor seed."
|
||||
return 0
|
||||
fi
|
||||
|
||||
until sudo docker exec "${uptime_kuma_container}" test -s /app/data/kuma.db >/dev/null 2>&1; do
|
||||
if ((elapsed >= 180)); then
|
||||
echo "Timed out waiting for Uptime Kuma database; skipping monitor seed." >&2
|
||||
return 0
|
||||
fi
|
||||
sleep 5
|
||||
elapsed=$((elapsed + 5))
|
||||
done
|
||||
|
||||
sudo docker cp "${monitor_file}" "${uptime_kuma_container}:/tmp/homelab-kuma-monitors.json"
|
||||
sudo docker cp "${seed_script}" "${uptime_kuma_container}:/tmp/homelab-seed-kuma.js"
|
||||
if sudo docker exec -w /app "${uptime_kuma_container}" node /tmp/homelab-seed-kuma.js /tmp/homelab-kuma-monitors.json; then
|
||||
sudo docker restart "${uptime_kuma_container}" >/dev/null
|
||||
else
|
||||
echo "Uptime Kuma monitor seed failed; leaving the running service unchanged." >&2
|
||||
fi
|
||||
}
|
||||
|
||||
install_missing_packages ca-certificates curl iptables python3 rsync
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
|
|
@ -223,6 +252,8 @@ fi
|
|||
sudo mkdir -p "${INSTALL_DIR}" "${DATA_DIR}/pihole/etc-pihole" "${DATA_DIR}/pihole/etc-dnsmasq.d" "${DATA_DIR}/uptime-kuma"
|
||||
sudo cp "${SCRIPT_DIR}/docker-compose.yml" "${INSTALL_DIR}/docker-compose.yml"
|
||||
sudo cp "${SCRIPT_DIR}/adlists.txt" "${INSTALL_DIR}/adlists.txt"
|
||||
sudo cp "${SCRIPT_DIR}/uptime-kuma-monitors.json" "${INSTALL_DIR}/uptime-kuma-monitors.json"
|
||||
sudo cp "${SCRIPT_DIR}/seed-uptime-kuma.js" "${INSTALL_DIR}/seed-uptime-kuma.js"
|
||||
write_env_file
|
||||
|
||||
stop_legacy_pihole
|
||||
|
|
@ -231,4 +262,5 @@ cd "${INSTALL_DIR}"
|
|||
sudo docker compose pull
|
||||
sudo docker compose up -d --remove-orphans
|
||||
sync_pihole_adlists
|
||||
seed_uptime_kuma_monitors
|
||||
sudo docker compose ps
|
||||
|
|
|
|||
|
|
@ -0,0 +1,174 @@
|
|||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
function loadDatabaseModule() {
|
||||
for (const moduleName of ["better-sqlite3", "/app/node_modules/better-sqlite3"]) {
|
||||
try {
|
||||
return require(moduleName);
|
||||
} catch (_error) {
|
||||
// Try the next location used by the upstream Uptime Kuma image.
|
||||
}
|
||||
}
|
||||
throw new Error("Could not load better-sqlite3 from the Uptime Kuma container");
|
||||
}
|
||||
|
||||
function nowSql() {
|
||||
return new Date().toISOString().replace("T", " ").slice(0, 19);
|
||||
}
|
||||
|
||||
function defaultForColumn(column, monitor, userId) {
|
||||
const accepted = monitor.accepted_statuscodes || ["200-399"];
|
||||
const defaults = {
|
||||
name: monitor.name,
|
||||
type: monitor.type,
|
||||
active: 1,
|
||||
user_id: userId,
|
||||
interval: monitor.interval || 60,
|
||||
retry_interval: monitor.retry_interval || 60,
|
||||
resend_interval: monitor.resend_interval || 0,
|
||||
maxretries: monitor.maxretries || 3,
|
||||
accepted_statuscodes: JSON.stringify(accepted),
|
||||
url: monitor.url || "",
|
||||
method: monitor.method || "GET",
|
||||
hostname: monitor.hostname || "",
|
||||
port: monitor.port || null,
|
||||
dns_resolve_type: monitor.dns_resolve_type || "A",
|
||||
dns_resolve_server: monitor.dns_resolve_server || "1.1.1.1",
|
||||
upside_down: 0,
|
||||
weight: monitor.weight || 2000,
|
||||
maxredirects: monitor.maxredirects || 10,
|
||||
timeout: monitor.timeout || 48,
|
||||
expiryNotification: 0,
|
||||
ignoreTls: monitor.ignoreTls ? 1 : 0,
|
||||
created_date: nowSql(),
|
||||
modified_date: nowSql(),
|
||||
body: "",
|
||||
headers: "",
|
||||
basic_auth_user: "",
|
||||
basic_auth_pass: "",
|
||||
authMethod: "",
|
||||
mqtt_success_message: "",
|
||||
database_connection_string: "",
|
||||
database_query: "",
|
||||
packet_size: 56
|
||||
};
|
||||
|
||||
return Object.prototype.hasOwnProperty.call(defaults, column.name)
|
||||
? defaults[column.name]
|
||||
: "";
|
||||
}
|
||||
|
||||
function monitorValues(monitor, columns, userId, includeCreatedDate) {
|
||||
const values = {};
|
||||
const supported = new Set(columns.map((column) => column.name));
|
||||
|
||||
const requested = {
|
||||
name: monitor.name,
|
||||
type: monitor.type,
|
||||
active: 1,
|
||||
user_id: userId,
|
||||
interval: monitor.interval || 60,
|
||||
retry_interval: monitor.retry_interval || 60,
|
||||
resend_interval: monitor.resend_interval || 0,
|
||||
maxretries: monitor.maxretries || 3,
|
||||
accepted_statuscodes: JSON.stringify(monitor.accepted_statuscodes || ["200-399"]),
|
||||
method: monitor.method || "GET",
|
||||
url: monitor.url || "",
|
||||
hostname: monitor.hostname || "",
|
||||
port: monitor.port || null,
|
||||
dns_resolve_type: monitor.dns_resolve_type || "A",
|
||||
dns_resolve_server: monitor.dns_resolve_server || "1.1.1.1",
|
||||
maxredirects: monitor.maxredirects || 10,
|
||||
timeout: monitor.timeout || 48,
|
||||
ignoreTls: monitor.ignoreTls ? 1 : 0,
|
||||
modified_date: nowSql()
|
||||
};
|
||||
|
||||
if (includeCreatedDate) {
|
||||
requested.created_date = nowSql();
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(requested)) {
|
||||
if (supported.has(key)) {
|
||||
values[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
for (const column of columns) {
|
||||
if (column.pk || Object.prototype.hasOwnProperty.call(values, column.name)) {
|
||||
continue;
|
||||
}
|
||||
if (column.notnull && column.dflt_value === null) {
|
||||
values[column.name] = defaultForColumn(column, monitor, userId);
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
function upsertMonitor(database, monitor, columns, userId) {
|
||||
const existing = database
|
||||
.prepare("select id from monitor where user_id = ? and name = ? limit 1")
|
||||
.get(userId, monitor.name);
|
||||
|
||||
if (existing) {
|
||||
const values = monitorValues(monitor, columns, userId, false);
|
||||
delete values.name;
|
||||
delete values.user_id;
|
||||
const assignments = Object.keys(values).map((key) => `"${key}" = @${key}`);
|
||||
database
|
||||
.prepare(`update monitor set ${assignments.join(", ")} where id = @id`)
|
||||
.run({ ...values, id: existing.id });
|
||||
console.log(`updated ${monitor.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const values = monitorValues(monitor, columns, userId, true);
|
||||
const keys = Object.keys(values);
|
||||
database
|
||||
.prepare(
|
||||
`insert into monitor (${keys.map((key) => `"${key}"`).join(", ")}) values (${keys
|
||||
.map((key) => `@${key}`)
|
||||
.join(", ")})`
|
||||
)
|
||||
.run(values);
|
||||
console.log(`created ${monitor.name}`);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const monitorFile = process.argv[2] || "/tmp/homelab-kuma-monitors.json";
|
||||
const databasePath = process.argv[3] || "/app/data/kuma.db";
|
||||
const monitors = JSON.parse(fs.readFileSync(monitorFile, "utf8"));
|
||||
const Database = loadDatabaseModule();
|
||||
const database = new Database(databasePath);
|
||||
|
||||
const userTable = database
|
||||
.prepare("select name from sqlite_master where type = 'table' and name = 'user'")
|
||||
.get();
|
||||
if (!userTable) {
|
||||
console.log("Uptime Kuma user table is missing; skipping monitor seed");
|
||||
return;
|
||||
}
|
||||
|
||||
const user = database.prepare("select id from user order by id limit 1").get();
|
||||
if (!user) {
|
||||
console.log("Uptime Kuma initial setup has not been completed; skipping monitor seed");
|
||||
return;
|
||||
}
|
||||
|
||||
const columns = database.prepare("pragma table_info(monitor)").all();
|
||||
if (!columns.length) {
|
||||
throw new Error("Uptime Kuma monitor table is missing");
|
||||
}
|
||||
|
||||
const transaction = database.transaction(() => {
|
||||
for (const monitor of monitors) {
|
||||
upsertMonitor(database, monitor, columns, user.id);
|
||||
}
|
||||
});
|
||||
transaction();
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
[
|
||||
{
|
||||
"name": "Public Website",
|
||||
"type": "http",
|
||||
"url": "https://lab2025.duckdns.org/",
|
||||
"accepted_statuscodes": ["200-399"]
|
||||
},
|
||||
{
|
||||
"name": "Public Gitea",
|
||||
"type": "http",
|
||||
"url": "https://lab2025.duckdns.org/git/",
|
||||
"accepted_statuscodes": ["200-399", "404"]
|
||||
},
|
||||
{
|
||||
"name": "Traefik LAN",
|
||||
"type": "http",
|
||||
"url": "http://192.168.100.240/",
|
||||
"accepted_statuscodes": ["200-399", "404"]
|
||||
},
|
||||
{
|
||||
"name": "Gitea LAN",
|
||||
"type": "http",
|
||||
"url": "http://192.168.100.73:3000/",
|
||||
"accepted_statuscodes": ["200-399", "404"]
|
||||
},
|
||||
{
|
||||
"name": "Pi-hole Web",
|
||||
"type": "http",
|
||||
"url": "http://192.168.100.89:8081/admin/",
|
||||
"accepted_statuscodes": ["200-399", "401", "403"]
|
||||
},
|
||||
{
|
||||
"name": "RPi DNS TCP 53",
|
||||
"type": "port",
|
||||
"hostname": "192.168.100.89",
|
||||
"port": 53
|
||||
},
|
||||
{
|
||||
"name": "RPi DNS UDP 53",
|
||||
"type": "dns",
|
||||
"hostname": "cloudflare.com",
|
||||
"port": 53,
|
||||
"dns_resolve_type": "A",
|
||||
"dns_resolve_server": "192.168.100.89"
|
||||
},
|
||||
{
|
||||
"name": "ARR Prowlarr",
|
||||
"type": "http",
|
||||
"url": "http://192.168.100.73:9696/",
|
||||
"accepted_statuscodes": ["200-399", "401", "403"]
|
||||
},
|
||||
{
|
||||
"name": "ARR Radarr",
|
||||
"type": "http",
|
||||
"url": "http://192.168.100.73:7878/",
|
||||
"accepted_statuscodes": ["200-399", "401", "403"]
|
||||
},
|
||||
{
|
||||
"name": "ARR Sonarr",
|
||||
"type": "http",
|
||||
"url": "http://192.168.100.73:8989/",
|
||||
"accepted_statuscodes": ["200-399", "401", "403"]
|
||||
},
|
||||
{
|
||||
"name": "ARR qBittorrent",
|
||||
"type": "http",
|
||||
"url": "http://192.168.100.73:8080/",
|
||||
"accepted_statuscodes": ["200-399", "401", "403"]
|
||||
},
|
||||
{
|
||||
"name": "ARR Kapowarr",
|
||||
"type": "http",
|
||||
"url": "http://192.168.100.73:5656/",
|
||||
"accepted_statuscodes": ["200-399", "401", "403"]
|
||||
},
|
||||
{
|
||||
"name": "ARR Suwayomi",
|
||||
"type": "http",
|
||||
"url": "http://192.168.100.73:4567/",
|
||||
"accepted_statuscodes": ["200-399", "401", "403"]
|
||||
},
|
||||
{
|
||||
"name": "ARR Maintainerr",
|
||||
"type": "http",
|
||||
"url": "http://192.168.100.73:6246/",
|
||||
"accepted_statuscodes": ["200-399", "401", "403"]
|
||||
},
|
||||
{
|
||||
"name": "ARR FlareSolverr",
|
||||
"type": "http",
|
||||
"url": "http://192.168.100.73:8191/",
|
||||
"accepted_statuscodes": ["200-399", "401", "403"]
|
||||
}
|
||||
]
|
||||
2
jeannie
2
jeannie
|
|
@ -2265,6 +2265,8 @@ deploy_rpi_services() {
|
|||
scp -i "${rpi_key}" -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \
|
||||
"${source_dir}/docker-compose.yml" \
|
||||
"${source_dir}/adlists.txt" \
|
||||
"${source_dir}/uptime-kuma-monitors.json" \
|
||||
"${source_dir}/seed-uptime-kuma.js" \
|
||||
"${source_dir}/bootstrap.sh" \
|
||||
"${rpi_user}@${rpi_host}:/tmp/homelab-rpi-services/"
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue