mirror of
https://github.com/Joulenap/joulenap.git
synced 2026-08-11 13:21:43 +02:00
A backup server serving two datastores is two devices, which the duplicate guard deliberately allows -- but both wanted a token called joulenap, so setting up the second one deleted and recreated the first one's token. The first device was left holding a dead secret, and re-entering the new one did not repair it: deleting a token also drops its ACL entries, and provisioning re-grants only /datastore/<its own datastore>, so the other datastore stayed locked out until root ran acl update by hand. The product supported a configuration its own wizard could not provision. Tokens on a backup server are now named joulenap-<datastore>, sanitised to the character set PBS accepts and falling back to the bare prefix if nothing survives. The two never meet, and each keeps the narrow per-datastore grant rather than widening to /datastore. A Proxmox host is a single device and cannot collide with itself, so its token stays plain joulenap. The name is derived rather than exposed: a field would only invite tokens Joulenap later fails to find. Tokens already in use are untouched. The conflict dialog no longer claims the name is "joulenap", since on a backup server it is not. A device card also stops reporting "Connected - API OK" for what is a one second TCP connect to the API port. The authenticated call behind it is made and its failure discarded, so a server whose credential had been revoked advertised itself as healthy indefinitely, with cached usage figures beside it to match. The label now reads "Reachable", which is what is actually checked; the Test button, which surfaces the same call's error, owns the API verdict. Changing the underlying field was rejected: it is a published contract, both in the dashboard payload and as joulenap_pbs_online, documented as answering on the API port. Documented in the architecture, the wizard guide and the example config, including that replacing a token clears its permissions -- so a hand-made setup where one token served several datastores needs re-granting.
205 lines
10 KiB
YAML
205 lines
10 KiB
YAML
# =====================================================================
|
|
# Joulenap — example configuration
|
|
# Copy to config.yaml and fill in your values. config.yaml is git-ignored.
|
|
# All secrets (tokens, passwords, ssh key) live here — keep it private.
|
|
# =====================================================================
|
|
|
|
app:
|
|
language: en # UI language: en, it, ...
|
|
theme: dark # dark | light — also switchable from the header toggle
|
|
port: 8080 # web UI port
|
|
scheduler_enabled: true # global kill-switch: false arms no route at all (manual runs
|
|
# still work). Pause one route with routes[].enabled: false.
|
|
timezone: "" # IANA tz for the scheduler, e.g. "Europe/Rome".
|
|
# Empty -> the TZ env var, then UTC. Set this (or TZ) or
|
|
# "backup at 02:00" runs at 02:00 UTC in a container.
|
|
secret_key: "CHANGE_ME" # random string for session signing
|
|
# Read-only key for the machine-readable endpoints: GET /api/dashboard (dashboard
|
|
# widgets) and GET /metrics (Prometheus). Leave empty to disable both.
|
|
# Generate/rotate it from Settings -> Integrations in the UI.
|
|
api_key: ""
|
|
# Ask GitHub once a day whether a newer Joulenap release exists, and show a footer
|
|
# badge if so. Off by default: the app makes no outbound internet call unless you
|
|
# turn this on (Settings -> Advanced).
|
|
update_check: false
|
|
session:
|
|
https_only: false
|
|
max_age_days: 14
|
|
auth:
|
|
username: admin
|
|
# password is stored hashed (bcrypt); set it on first run via the UI, or
|
|
# pre-hash it here. Never store plaintext. Generate a hash with:
|
|
# docker exec -it joulenap python -m app.hashpw
|
|
# (or, from a local checkout: cd backend && python -m app.hashpw)
|
|
# Quote the value — the hash contains '$' characters.
|
|
password_hash: ""
|
|
|
|
# =====================================================================
|
|
# Devices and routes
|
|
#
|
|
# Devices are listed once and referenced by id. A *route* is "sources -> target +
|
|
# schedule" and covers every reason Joulenap wakes a PBS:
|
|
#
|
|
# backup one or more PVE sources -> a PBS target (vzdump)
|
|
# sync one PBS source -> another PBS target (remote + sync job)
|
|
# external no source -> a PBS target (PVE/PBS run their own
|
|
# scheduled jobs; Joulenap
|
|
# wakes, watches, powers off)
|
|
# verify no source -> a PBS target (verify its snapshots)
|
|
#
|
|
# All three lists start empty: the wizards under Settings -> Devices -> + Add fill in the
|
|
# devices, then you draw routes between them from the homepage. The commented blocks below
|
|
# show the full shape of each entry.
|
|
# =====================================================================
|
|
|
|
# --- Proxmox VE endpoints (where the guests live) ---
|
|
pves: []
|
|
# pves:
|
|
# - id: pve-01 # slug, unique; also the name shown in the UI
|
|
# host: 192.168.1.10 # or pve.lan. A cluster is ONE entry — it proxies its nodes,
|
|
# # which Joulenap discovers at runtime.
|
|
# port: 8006
|
|
# verify_tls: false # self-signed cert on most homelabs
|
|
# # Token privileges: VM.Audit, VM.Backup, Datastore.Audit, Datastore.AllocateSpace and
|
|
# # Datastore.Allocate on the PBS storage. The last one is required for vzdump's
|
|
# # retention/prune (it deletes old backups) — without it, backups run but pruning fails.
|
|
# api_token_id: "root@pam!joulenap"
|
|
# api_token_secret: ""
|
|
# # For each PBS this node backs up to: the storage as named here in PVE
|
|
# # (Datacenter > Storage). Different PVEs may name the same PBS differently.
|
|
# storages:
|
|
# pbs-01: pbs
|
|
|
|
# --- Proxmox Backup Servers (the boxes that sleep) ---
|
|
pbss: []
|
|
# pbss:
|
|
# - id: pbs-01
|
|
# host: 192.168.1.20 # or pbs.lan
|
|
# port: 8007
|
|
# datastore: backup
|
|
# # Joulenap reads the datastore's ROOT NAMESPACE. If the Proxmox storage entry writes
|
|
# # into a PBS namespace, backups, retention and GC all work normally, but "Last backup
|
|
# # per guest" reads "never" for those guests — Joulenap looks in the root and finds
|
|
# # nothing. One backup server can hold several datastores: add each as its own device.
|
|
# fingerprint: "" # PBS dashboard > Show Fingerprint, e.g. "aa:bb:cc:..."
|
|
# # Leave this empty and the connection to this PBS is NOT pinned and NOT validated —
|
|
# # its API token then travels over TLS nothing has checked. The wizard fills it in.
|
|
# # Token privileges: DatastoreAdmin on the datastore (status, GC, verify) plus Audit on
|
|
# # /system (read-only node CPU/RAM/network for the dashboard). A PBS taking part in a
|
|
# # sync route also needs RemoteAdmin AND RemoteSyncPushOperator on /remote — PBS refuses
|
|
# # ACL writes from a token, so those must be granted on the box itself or by the wizard's
|
|
# # root mode (see docs/CONFIG-WIZARD.md).
|
|
# # The wizard names a backup server's token after its datastore, so a second datastore on
|
|
# # the same machine gets its own instead of replacing this one.
|
|
# api_token_id: "root@pam!joulenap-backup"
|
|
# api_token_secret: ""
|
|
# # Set to false for an always-on PBS (a VM, or a cloud-hosted one): Joulenap then only
|
|
# # schedules routes onto it and never touches its power, so everything below is unused.
|
|
# managed_power: true
|
|
# # Wake-on-LAN
|
|
# mac: "00:11:22:33:44:55" # PBS NIC MAC
|
|
# wol_broadcast_iface: "" # interface on the Joulenap host used to send the magic packet, e.g. eth0 (blank = auto-detect)
|
|
# wait_timeout: 180 # seconds to wait for PBS to become reachable (per wake attempt)
|
|
# wol_retries: 2 # EXTRA re-sends after the first: total attempts = wol_retries + 1,
|
|
# # each waiting up to wait_timeout
|
|
# # Power-off (no PBS API for this -> SSH)
|
|
# poweroff_task_wait: 600 # wait up to N s for a running PBS task to finish before power-off (0 = no wait)
|
|
# ssh_user: root
|
|
# ssh_key_path: /app/data/id_ed25519 # dedicated key; ideally restricted to a poweroff command on PBS
|
|
# # How long External routes onto this PBS wait — i.e. how slow this box is.
|
|
# # Both are worst-case timeouts, not fixed delays.
|
|
# external:
|
|
# first_task_wait: 900 # wait at most N s for the first task to appear after wake-up
|
|
# idle_wait: 300 # power off after N s of continuous task silence
|
|
|
|
# --- Routes ---
|
|
routes: []
|
|
# routes:
|
|
# - id: nightly
|
|
# name: Nightly backup
|
|
# color: "#f5a524" # the route's colour in the topology diagram
|
|
# enabled: true
|
|
# notify: true # send notifications for this route (channels are global, below)
|
|
# kind: backup # backup | sync | external | verify
|
|
# # Guests are per source: vmids collide between PVEs, so each source picks its own.
|
|
# # A newly created VM/CT is backed up automatically in "all" mode but NOT in
|
|
# # "include" mode — that list is explicit, so add new guests to it yourself.
|
|
# sources:
|
|
# - pve: pve-01
|
|
# guests:
|
|
# mode: all # all | include
|
|
# list: [] # used when mode = include, e.g. [104, 106, 202]
|
|
# target: pbs-01
|
|
# schedule:
|
|
# time: "04:00"
|
|
# days: [true, true, true, true, true, true, true] # Mon..Sun; at least one must be
|
|
# # true (pause with enabled: false)
|
|
# # Escape hatch for a schedule time+days cannot express (a day-of-month or month
|
|
# # pattern, a step value, a weekday range). A 5-field crontab string; when set it
|
|
# # wins over time/days, and the UI shows it read-only.
|
|
# # cron: "0 4 1 * *"
|
|
# retention: # applied on the target
|
|
# keep_last: 0
|
|
# keep_daily: 7
|
|
# keep_weekly: 4
|
|
# keep_monthly: 6
|
|
# keep_yearly: 0
|
|
# options:
|
|
# mode: snapshot # snapshot | suspend | stop
|
|
# bwlimit: 0 # KiB/s, 0 = unlimited
|
|
# min_free_percent: 0 # abort if the target datastore has less than this % free (0 = disabled)
|
|
# gc: true # garbage-collect the target once this route is done. Two routes
|
|
# # onto the same PBS with gc on both will each run one.
|
|
# verify_after: false # quick verify of this run's new snapshots, before power-off
|
|
# reverify_days: 30 # verify routes only: skip snapshots verified in the last N
|
|
# # days, so the route stays incremental (0 = verify everything)
|
|
#
|
|
# # A sync route copies snapshots from one PBS to another (pull or push).
|
|
# - id: offsite
|
|
# name: Offsite copy
|
|
# color: "#3b82f6"
|
|
# kind: sync
|
|
# source_pbs: pbs-01
|
|
# target: pbs-02
|
|
# sync_direction: pull # pull | push
|
|
# schedule:
|
|
# time: "05:00"
|
|
# days: [false, false, false, false, false, false, true] # Sundays only
|
|
|
|
# --- Maintenance ---
|
|
# GC and post-backup verify are per-route (routes[].options.gc / .verify_after), and a
|
|
# periodic full verification is a route of kind "verify". Only history retention is global.
|
|
maintenance:
|
|
history:
|
|
retention_days: 14 # auto-delete run history + activity-log entries older than N days (0 = keep forever)
|
|
|
|
# --- Notifications ---
|
|
# Delivered via Apprise (one engine, 100+ services). The friendly fields below
|
|
# are turned into Apprise URLs under the hood; use custom_urls for anything else.
|
|
notifications:
|
|
on_success: true
|
|
on_failure: true
|
|
telegram:
|
|
enabled: false
|
|
bot_token: ""
|
|
chat_id: ""
|
|
ntfy:
|
|
enabled: false
|
|
url: "" # self-hosted, e.g. http://192.168.1.30, or https://ntfy.sh
|
|
topic: "" # e.g. homelab
|
|
email:
|
|
enabled: false # SMTP client
|
|
smtp_host: ""
|
|
smtp_port: 587
|
|
smtp_user: ""
|
|
smtp_password: ""
|
|
from_addr: "joulenap@example.com"
|
|
to_addr: ""
|
|
discord:
|
|
enabled: false
|
|
webhook_url: ""
|
|
# Catch-all: any Apprise URL (https://github.com/caronc/apprise) or a plain webhook.
|
|
# e.g. tgram://token/chatid, ntfy://host/topic, gotify://host/token,
|
|
# pover://user@token, slack://..., matrixs://..., json://your-endpoint
|
|
custom_urls: []
|