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
|
`infra/rpi-services/adlists.txt`; the bootstrap inserts those lists without
|
||||||
deleting manually added Pi-hole lists. If `PIHOLE_WEBPASSWORD` is not provided,
|
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`.
|
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,
|
`./jeannie status` and `./jeannie doctor-rpi` test Pi-hole DNS,
|
||||||
direct Unbound DNS, public fallback resolver readiness, Uptime Kuma HTTP, and
|
direct Unbound DNS, public fallback resolver readiness, Uptime Kuma HTTP, and
|
||||||
whether Docker is currently using the NVMe path or `/var/lib/docker`.
|
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
|
`infra/rpi-services/adlists.txt`; the bootstrap inserts those lists without
|
||||||
deleting manually added Pi-hole lists. If `PIHOLE_WEBPASSWORD` is not provided,
|
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`.
|
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,
|
`./{{ main_script }} status` and `./{{ main_script }} doctor-rpi` test Pi-hole DNS,
|
||||||
direct Unbound DNS, public fallback resolver readiness, Uptime Kuma HTTP, and
|
direct Unbound DNS, public fallback resolver readiness, Uptime Kuma HTTP, and
|
||||||
whether Docker is currently using the NVMe path or `/var/lib/docker`.
|
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
|
pihole_web_port: 8081
|
||||||
uptime_kuma_port: 3001
|
uptime_kuma_port: 3001
|
||||||
unbound_container_port: 53
|
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:
|
traefik:
|
||||||
host: kubernetes
|
host: kubernetes
|
||||||
load_balancer_ip: 192.168.100.240
|
load_balancer_ip: 192.168.100.240
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ Kubernetes cluster:
|
||||||
- Pi-hole on DNS port `53` and web port `8081`
|
- Pi-hole on DNS port `53` and web port `8081`
|
||||||
- Unbound as Pi-hole's first upstream resolver
|
- Unbound as Pi-hole's first upstream resolver
|
||||||
- Uptime Kuma on port `3001`
|
- 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
|
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.
|
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
|
Managed Pi-hole adlists are stored in `adlists.txt`. The bootstrap inserts them
|
||||||
without deleting existing manual Pi-hole lists.
|
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
|
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
|
install_missing_packages ca-certificates curl iptables python3 rsync
|
||||||
|
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
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 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}/docker-compose.yml" "${INSTALL_DIR}/docker-compose.yml"
|
||||||
sudo cp "${SCRIPT_DIR}/adlists.txt" "${INSTALL_DIR}/adlists.txt"
|
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
|
write_env_file
|
||||||
|
|
||||||
stop_legacy_pihole
|
stop_legacy_pihole
|
||||||
|
|
@ -231,4 +262,5 @@ cd "${INSTALL_DIR}"
|
||||||
sudo docker compose pull
|
sudo docker compose pull
|
||||||
sudo docker compose up -d --remove-orphans
|
sudo docker compose up -d --remove-orphans
|
||||||
sync_pihole_adlists
|
sync_pihole_adlists
|
||||||
|
seed_uptime_kuma_monitors
|
||||||
sudo docker compose ps
|
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 \
|
scp -i "${rpi_key}" -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \
|
||||||
"${source_dir}/docker-compose.yml" \
|
"${source_dir}/docker-compose.yml" \
|
||||||
"${source_dir}/adlists.txt" \
|
"${source_dir}/adlists.txt" \
|
||||||
|
"${source_dir}/uptime-kuma-monitors.json" \
|
||||||
|
"${source_dir}/seed-uptime-kuma.js" \
|
||||||
"${source_dir}/bootstrap.sh" \
|
"${source_dir}/bootstrap.sh" \
|
||||||
"${rpi_user}@${rpi_host}:/tmp/homelab-rpi-services/"
|
"${rpi_user}@${rpi_host}:/tmp/homelab-rpi-services/"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue